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
|
||||
//! 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};
|
||||
|
||||
@@ -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