Modulstruktur: storage/sql/compression/merge/query/txn + tests/examples/docs
CI / build-and-test (push) Successful in 47s

This commit is contained in:
2026-06-13 01:36:32 +02:00
parent 36df095fac
commit 81bdbe5e64
13 changed files with 242 additions and 98 deletions
+2
View File
@@ -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.
+33
View File
@@ -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.
+15
View File
@@ -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);
}
}
+11
View File
@@ -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;
+18 -98
View File
@@ -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<Row>,
}
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};
+6
View File
@@ -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.
+10
View File
@@ -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;
+10
View File
@@ -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
+92
View File
@@ -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<Row>,
}
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());
}
}
+12
View File
@@ -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
+12
View File
@@ -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,
}
+5
View File
@@ -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.
+16
View File
@@ -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());
}