diff --git a/benches/.gitkeep b/benches/.gitkeep new file mode 100644 index 0000000..f27960f --- /dev/null +++ b/benches/.gitkeep @@ -0,0 +1,2 @@ +# Platzhalter – hier kommen später Benchmarks rein (z. B. mit criterion). +# Bewusst noch keine .rs-Datei, damit cargo nichts Leeres zu bauen versucht. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..4d6f966 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,33 @@ +# paulDB – Architektur + +> Internes Design-Dokument. Das „Warum" hinter der Ordnerstruktur. + +## Big Picture + +paulDB ist eine **In-Memory HTAP-Engine**: Row- und Column-Storage in einem +System, verbunden durch einen Delta-Merge nach SAP-HANA-Vorbild. + +``` +SQL-Frontend ──> Query-Router ──┬─> Row-Pfad (Delta) ─┐ + (sql) (query) │ ├─> Ergebnis + └─> Column-Pfad (Main) ─┘ + Delta ──(Delta-Merge)──> Main +``` + +## Module ↔ Roadmap + +| Modul (`src/`) | Etappe | Aufgabe | +|--------------------|--------|---------| +| `storage/delta` | E1 | schreib-optimierter Row-Store (OLTP) | +| `sql/` | E2/E6 | Tokenizer → AST → Plan | +| `storage/main` | E3 | lese-optimierter Column-Store (OLAP) | +| `compression/` | E3 | Dictionary + Prefix/RLE/Cluster/Sparse/Indirect | +| `merge/` | E4 | Delta → Main (das Herzstück) | +| `query/` | E5 | Router: OLTP- vs OLAP-Pfad (der HTAP-Beweis) | +| `txn/` | E7 | MVCC / konsistente Lesersicht | + +## Prinzipien + +- **Verstehen vor Benutzen** – jedes Modul kommentiert das Warum. +- **Klein, aber echt** – lieber wenig, das wirklich läuft. +- **Schlanke Public API** – Re-Exports in `lib.rs`, Details bleiben privat. diff --git a/examples/demo_delta.rs b/examples/demo_delta.rs new file mode 100644 index 0000000..70b156b --- /dev/null +++ b/examples/demo_delta.rs @@ -0,0 +1,15 @@ +//! Beispiel: der Delta-Row-Store in Aktion. +//! Starten mit: cargo run --example demo_delta + +use pauldb::{Delta, Row}; + +fn main() { + let mut delta = Delta::new(); + delta.insert(Row { id: 1, kategorie: "ABAP".into(), wert: 100.0 }); + delta.insert(Row { id: 2, kategorie: "HANA".into(), wert: 250.5 }); + + println!("paulDB Demo – {} Zeilen", delta.len()); + for row in delta.scan() { + println!(" #{:<3} {:<6} {:>8.2}", row.id, row.kategorie, row.wert); + } +} diff --git a/src/compression/mod.rs b/src/compression/mod.rs new file mode 100644 index 0000000..a751e0c --- /dev/null +++ b/src/compression/mod.rs @@ -0,0 +1,11 @@ +//! Spalten-Kompression für den Main-Store (Etappe E3). +//! +//! Zweistufig nach HANA-Vorbild: +//! 1. Dictionary-Encoding (Basis): distinct-Werte + Integer-Value-IDs. +//! 2. Advanced Compression auf die Value-IDs: +//! Prefix · Run-Length (RLE) · Cluster · Sparse · Indirect. +//! +//! Noch leer – folgt in E3. + +// pub mod dictionary; +// pub mod rle; diff --git a/src/lib.rs b/src/lib.rs index 3e969e8..8c29c5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,102 +1,22 @@ -//! paulDB – Etappe E1 (Anfang): ein winziger In-Memory Row-Store, das "Delta". +//! # paulDB 🦀 //! -//! Warum der Row-Store zuerst? Eine Zeile anzuhängen ist billig – genau das macht -//! die OLTP-/Schreib-Seite (Delta) bei SAP HANA schnell. Später kommt der -//! komprimierte Column-Store (Main) dazu, verbunden über den Delta-Merge. +//! Eine eigene HTAP-Datenbank in Rust: Row- und Column-Storage in einer +//! In-Memory-Engine, verbunden über ein Delta-Merge-Konzept nach HANA-Vorbild. //! -//! Bewusst minimal: festes Schema, kein Index, kein SQL. Das alles sind eigene, -//! spätere Etappen (siehe README-Roadmap). Hier geht es nur darum, das Fundament -//! laufen und testen zu lassen. +//! Die Subsysteme spiegeln die Roadmap wider: +//! - [`storage`] – Delta (Row, E1) + Main (Column, E3) +//! - [`sql`] – SQL-Frontend (E2/E6) +//! - [`compression`] – Spalten-Kompression (E3) +//! - [`merge`] – Delta-Merge (E4) +//! - [`query`] – Query-Router (E5) +//! - [`txn`] – Transaktionen / MVCC (E7) -/// Eine Zeile unserer ersten, bewusst simplen Tabelle. -/// (Das Schema ist hart verdrahtet – ein echter Katalog kommt in einer späteren Etappe.) -#[derive(Debug, Clone, PartialEq)] -pub struct Row { - pub id: u64, - pub kategorie: String, - pub wert: f64, -} +pub mod compression; +pub mod merge; +pub mod query; +pub mod sql; +pub mod storage; +pub mod txn; -/// Der Delta-Store: schreib-optimiert. Wir hängen Zeilen einfach hinten an. -#[derive(Debug, Default)] -pub struct Delta { - rows: Vec, -} - -impl Delta { - /// Ein neuer, leerer Delta-Store. - pub fn new() -> Self { - Delta { rows: Vec::new() } - } - - /// Zeile anhängen – amortisiert O(1). Das ist die OLTP-Stärke des Row-Stores. - pub fn insert(&mut self, row: Row) { - self.rows.push(row); - } - - /// Full-Scan über alle Zeilen – O(n). Noch ohne Index; - /// ein B-Tree-Index (O(log n)) ist eine spätere Etappe. - pub fn scan(&self) -> &[Row] { - &self.rows - } - - /// Punkt-Lookup über die id (vorerst linear). Zeigt schon den OLTP-Zugriffspfad, - /// den der Query-Router (E5) später gezielt bedienen wird. - pub fn find_by_id(&self, id: u64) -> Option<&Row> { - self.rows.iter().find(|r| r.id == id) - } - - /// Anzahl gespeicherter Zeilen. - pub fn len(&self) -> usize { - self.rows.len() - } - - /// Ist der Store leer? - pub fn is_empty(&self) -> bool { - self.rows.is_empty() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn beispiel() -> Delta { - let mut d = Delta::new(); - d.insert(Row { id: 1, kategorie: "ABAP".into(), wert: 100.0 }); - d.insert(Row { id: 2, kategorie: "HANA".into(), wert: 250.5 }); - d.insert(Row { id: 3, kategorie: "ABAP".into(), wert: 75.0 }); - d - } - - #[test] - fn neuer_store_ist_leer() { - let d = Delta::new(); - assert!(d.is_empty()); - assert_eq!(d.len(), 0); - } - - #[test] - fn insert_erhoeht_len() { - let d = beispiel(); - assert_eq!(d.len(), 3); - assert!(!d.is_empty()); - } - - #[test] - fn scan_gibt_alle_zeilen_in_einfuege_reihenfolge() { - let d = beispiel(); - let alle = d.scan(); - assert_eq!(alle.len(), 3); - assert_eq!(alle[0].id, 1); - assert_eq!(alle[2].kategorie, "ABAP"); - } - - #[test] - fn punkt_lookup_findet_und_verfehlt() { - let d = beispiel(); - let treffer = d.find_by_id(2).expect("id 2 sollte existieren"); - assert_eq!(treffer.kategorie, "HANA"); - assert!(d.find_by_id(99).is_none()); - } -} +// Bequeme Re-Exports, damit `use pauldb::{Delta, Row};` direkt funktioniert. +pub use storage::{Delta, Row}; diff --git a/src/merge/mod.rs b/src/merge/mod.rs new file mode 100644 index 0000000..6a784a6 --- /dev/null +++ b/src/merge/mod.rs @@ -0,0 +1,6 @@ +//! Delta-Merge (Etappe E4) – das Herzstück. +//! +//! Schiebt periodisch Daten aus dem schreib-optimierten Delta (Row) +//! in den lese-optimierten Main (Column) und leert anschließend den Delta. +//! +//! Noch leer – folgt in E4. diff --git a/src/query/mod.rs b/src/query/mod.rs new file mode 100644 index 0000000..676a96e --- /dev/null +++ b/src/query/mod.rs @@ -0,0 +1,10 @@ +//! Query-Router, Planner & Executor (Etappe E5) – der HTAP-Beweis. +//! +//! Entscheidet pro Query den Pfad: +//! Punktabfrage / INSERT -> Row-Pfad (Delta + Main) +//! Aggregation / GROUP BY -> Column-Pfad (Main) +//! +//! Noch leer – folgt in E5. + +// pub mod router; +// pub mod executor; diff --git a/src/sql/mod.rs b/src/sql/mod.rs new file mode 100644 index 0000000..5b2fdee --- /dev/null +++ b/src/sql/mod.rs @@ -0,0 +1,10 @@ +//! SQL-Frontend (Etappe E2, Ausbau E6). +//! +//! Hier entsteht der Weg von Text zu ausführbarem Plan: +//! Tokenizer (lexer) → AST (parser/ast) → später an den `query::router`. +//! +//! Noch leer – wird in E2 mit Leben gefüllt. + +// pub mod lexer; // Text -> Tokens +// pub mod ast; // AST-Typen +// pub mod parser; // Tokens -> AST diff --git a/src/storage/delta.rs b/src/storage/delta.rs new file mode 100644 index 0000000..7ffdc40 --- /dev/null +++ b/src/storage/delta.rs @@ -0,0 +1,92 @@ +//! Delta: in-memory, schreib-optimierter Row-Store (Etappe E1). +//! +//! Warum Row-Store zuerst? Eine Zeile anzuhängen ist billig – genau das macht +//! die OLTP-/Schreib-Seite (Delta) bei SAP HANA schnell. Der komprimierte +//! Column-Store (Main) kommt in E3 dazu, verbunden über den Delta-Merge (E4). + +use crate::storage::Row; + +/// Der Delta-Store: schreib-optimiert. Zeilen werden einfach hinten angehängt. +#[derive(Debug, Default)] +pub struct Delta { + rows: Vec, +} + +impl Delta { + /// Ein neuer, leerer Delta-Store. + pub fn new() -> Self { + Delta { rows: Vec::new() } + } + + /// Zeile anhängen – amortisiert O(1). Die OLTP-Stärke des Row-Stores. + pub fn insert(&mut self, row: Row) { + self.rows.push(row); + } + + /// Full-Scan über alle Zeilen – O(n). Noch ohne Index (B-Tree-Index = O(log n) + /// ist eine spätere Etappe). + pub fn scan(&self) -> &[Row] { + &self.rows + } + + /// Punkt-Lookup über die id (vorerst linear). Zeigt schon den OLTP-Zugriffspfad, + /// den der Query-Router (E5) später gezielt bedient. + pub fn find_by_id(&self, id: u64) -> Option<&Row> { + self.rows.iter().find(|r| r.id == id) + } + + /// Anzahl gespeicherter Zeilen. + pub fn len(&self) -> usize { + self.rows.len() + } + + /// Ist der Store leer? + pub fn is_empty(&self) -> bool { + self.rows.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::Delta; + use crate::storage::Row; + + fn beispiel() -> Delta { + let mut d = Delta::new(); + d.insert(Row { id: 1, kategorie: "ABAP".into(), wert: 100.0 }); + d.insert(Row { id: 2, kategorie: "HANA".into(), wert: 250.5 }); + d.insert(Row { id: 3, kategorie: "ABAP".into(), wert: 75.0 }); + d + } + + #[test] + fn neuer_store_ist_leer() { + let d = Delta::new(); + assert!(d.is_empty()); + assert_eq!(d.len(), 0); + } + + #[test] + fn insert_erhoeht_len() { + let d = beispiel(); + assert_eq!(d.len(), 3); + assert!(!d.is_empty()); + } + + #[test] + fn scan_gibt_alle_zeilen_in_einfuege_reihenfolge() { + let d = beispiel(); + let alle = d.scan(); + assert_eq!(alle.len(), 3); + assert_eq!(alle[0].id, 1); + assert_eq!(alle[2].kategorie, "ABAP"); + } + + #[test] + fn punkt_lookup_findet_und_verfehlt() { + let d = beispiel(); + let treffer = d.find_by_id(2).expect("id 2 sollte existieren"); + assert_eq!(treffer.kategorie, "HANA"); + assert!(d.find_by_id(99).is_none()); + } +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..1163c02 --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,12 @@ +//! Speicher-Subsystem. +//! +//! - [`Delta`] – schreib-optimierter Row-Store (Etappe E1) +//! - Main (lese-optimierter Column-Store, E3) folgt hier als eigenes Modul. + +mod delta; +mod row; + +pub use delta::Delta; +pub use row::Row; + +// pub mod main_store; // E3: lese-optimierter, komprimierter Column-Store diff --git a/src/storage/row.rs b/src/storage/row.rs new file mode 100644 index 0000000..773173b --- /dev/null +++ b/src/storage/row.rs @@ -0,0 +1,12 @@ +//! Zeilen- und Schema-Typen. +//! +//! Vorerst ein festes Schema. Ein echtes Katalog-System (Tabellen, Spalten, +//! Typen) ist eine spätere Etappe. + +/// Eine Zeile der ersten, bewusst simplen Tabelle. +#[derive(Debug, Clone, PartialEq)] +pub struct Row { + pub id: u64, + pub kategorie: String, + pub wert: f64, +} diff --git a/src/txn/mod.rs b/src/txn/mod.rs new file mode 100644 index 0000000..82fd653 --- /dev/null +++ b/src/txn/mod.rs @@ -0,0 +1,5 @@ +//! Transaktionen & MVCC (Etappe E7). +//! +//! Sorgt für eine konsistente Lesersicht – auch während ein Delta-Merge läuft. +//! +//! Noch leer – folgt in E7. diff --git a/tests/delta_integration.rs b/tests/delta_integration.rs new file mode 100644 index 0000000..02627b3 --- /dev/null +++ b/tests/delta_integration.rs @@ -0,0 +1,16 @@ +//! Integrationstest: nutzt paulDB nur über die öffentliche API +//! (so, wie ein externer Nutzer das Crate verwenden würde). + +use pauldb::{Delta, Row}; + +#[test] +fn insert_scan_und_lookup_ueber_public_api() { + let mut delta = Delta::new(); + delta.insert(Row { id: 10, kategorie: "SAP".into(), wert: 42.0 }); + delta.insert(Row { id: 20, kategorie: "Rust".into(), wert: 7.0 }); + + assert_eq!(delta.len(), 2); + assert_eq!(delta.scan().len(), 2); + assert_eq!(delta.find_by_id(20).unwrap().kategorie, "Rust"); + assert!(delta.find_by_id(999).is_none()); +}