Modulstruktur: storage/sql/compression/merge/query/txn + tests/examples/docs
CI / build-and-test (push) Successful in 47s
CI / build-and-test (push) Successful in 47s
This commit is contained in:
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
//! Eine eigene HTAP-Datenbank in Rust: Row- und Column-Storage in einer
|
||||||
//! die OLTP-/Schreib-Seite (Delta) bei SAP HANA schnell. Später kommt der
|
//! In-Memory-Engine, verbunden über ein Delta-Merge-Konzept nach HANA-Vorbild.
|
||||||
//! komprimierte Column-Store (Main) dazu, verbunden über den Delta-Merge.
|
|
||||||
//!
|
//!
|
||||||
//! Bewusst minimal: festes Schema, kein Index, kein SQL. Das alles sind eigene,
|
//! Die Subsysteme spiegeln die Roadmap wider:
|
||||||
//! spätere Etappen (siehe README-Roadmap). Hier geht es nur darum, das Fundament
|
//! - [`storage`] – Delta (Row, E1) + Main (Column, E3)
|
||||||
//! laufen und testen zu lassen.
|
//! - [`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.
|
pub mod compression;
|
||||||
/// (Das Schema ist hart verdrahtet – ein echter Katalog kommt in einer späteren Etappe.)
|
pub mod merge;
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
pub mod query;
|
||||||
pub struct Row {
|
pub mod sql;
|
||||||
pub id: u64,
|
pub mod storage;
|
||||||
pub kategorie: String,
|
pub mod txn;
|
||||||
pub wert: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Der Delta-Store: schreib-optimiert. Wir hängen Zeilen einfach hinten an.
|
// Bequeme Re-Exports, damit `use pauldb::{Delta, Row};` direkt funktioniert.
|
||||||
#[derive(Debug, Default)]
|
pub use storage::{Delta, Row};
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user