From ea49af351dcd8235add0bba45a97f14c1d3cc0bb Mon Sep 17 00:00:00 2001 From: paulhorn Date: Sat, 13 Jun 2026 00:30:36 +0200 Subject: [PATCH] =?UTF-8?q?E0/E1:=20Cargo-Ger=C3=BCst=20+=20In-Memory=20De?= =?UTF-8?q?lta-Row-Store=20mit=20Tests=20+=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 25 ++++++++++ .gitignore | 5 ++ Cargo.lock | 7 +++ Cargo.toml | 9 ++++ src/lib.rs | 102 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 25 ++++++++++ 6 files changed, 173 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..eab7a06 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,25 @@ +# paulDB – Gitea Actions CI +# Läuft auf deinem act_runner (server2) im Image rust:1 (per Runner-Label gemappt). + +name: CI + +on: [push, pull_request] + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Code auschecken + uses: actions/checkout@v4 + + - name: Toolchain anzeigen + run: rustc --version && cargo --version + + - name: Format prüfen (nicht blockierend) + run: cargo fmt --all --check || true + + - name: Bauen + run: cargo build --verbose + + - name: Tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2100db9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Rust-Build-Artefakte +/target + +# IntelliJ / RustRover +/.idea/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3f8e404 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "pauldb" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6d58fae --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pauldb" +version = "0.1.0" +edition = "2021" +description = "Eine eigene HTAP-Datenbank in Rust – Row+Column Storage, Delta-Merge (HANA-inspiriert)." +license = "MIT" + +# Noch keine Abhängigkeiten – paulDB wird bewusst from scratch gebaut. +[dependencies] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3e969e8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,102 @@ +//! paulDB – Etappe E1 (Anfang): ein winziger In-Memory Row-Store, das "Delta". +//! +//! 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. +//! +//! 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. + +/// 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, +} + +/// 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()); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d87af27 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,25 @@ +//! Kleines Demo-Programm: der Delta-Row-Store in Aktion. +//! `cargo run` -> ein paar Inserts, ein Full-Scan und ein Punkt-Lookup. + +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 }); + delta.insert(Row { id: 3, kategorie: "ABAP".into(), wert: 75.0 }); + + println!("paulDB – Delta-Row-Store ({} Zeilen)\n", delta.len()); + + println!("Full-Scan (OLTP-Pfad, O(n)):"); + for row in delta.scan() { + println!(" #{:<3} {:<6} {:>8.2}", row.id, row.kategorie, row.wert); + } + + if let Some(row) = delta.find_by_id(2) { + println!("\nPunkt-Lookup id=2 -> {} / {:.2}", row.kategorie, row.wert); + } + + println!("\nNächste Etappe: Mini-SQL-Parser (E2), dann der Column-Store (E3)."); +}