Files
rdb/README.md
T
wkh df8bcdfaf1 Handbuch-Beispiele als Test, search/seek mit Schlüsseltabelle
- doc/handbuch.txt: OCR-Text des COHERENT-/rdb-Handbuchs
- demo/handbuch/ und test/handbuch_test.rb: Beispiele aus dem Lehrbuchteil, mise run test führt sie mit aus
- search/seek: Schlüsselspalten aus dem Kopf der Schlüsseltabelle
- gregorian -u ohne führende Null im Monat, mean ganzzahlig ohne .00
- Mit justify aufgefüllte Spaltennamen werden wieder gefunden
- coherent.pdf in .gitignore
2026-10-14 21:48:46 +02:00

290 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# rdb
Ein Nachbau von **/rdb**, dem relationalen Datenbanksystem der Mark Williams
Company für COHERENT (um 1991), in Ruby. Das Original-Handbuch und die
Man-Pages stammen aus den freigegebenen MWC-Quellen
([nesssoftware.com/home/mwc/source.php](https://www.nesssoftware.com/home/mwc/source.php)).
Der Programmcode von /rdb selbst war darin nicht enthalten. Alle Befehle hier
sind nach dem Handbuch neu geschrieben.
## Einrichten
```
mise trust # einmal nach dem Clone: mise.toml freigeben
mise install # Ruby 3.4
mise run install # legt die Symlinks row, column, total ... in bin/ an
mise run test # 114 Tests und die Beispiele aus dem Handbuch
```
Ohne `mise trust` brechen in einem frisch geklonten Ordner alle `mise`-Befehle ab.
> **Wichtig: Nach jedem Clone zuerst die Symlinks anlegen.**
> Im Repository liegt nur `bin/rdb`. Die Befehle `row`, `column`, `total` ...
> sind Symlinks darauf und werden **nicht** mit eingecheckt (`.gitignore`).
> Ohne `mise run install` (oder ohne mise: `bin/rdb install`) gibt es sie nicht,
> und Aufrufe wie `row ...` enden mit `command not found`. Auch `testall` und
> die Tests brauchen sie. Nach neuen Befehlen den Schritt wiederholen.
Danach steht `bin/` im PATH, solange man im Projektordner ist (`mise.toml`).
Jeder Befehl ist ein Symlink auf `bin/rdb`, das am Aufrufnamen erkennt, was zu
tun ist. Es geht auch ohne Symlinks: `rdb row 'Cost > 5' < demo/inventory`.
Vier Namen verdecken im Projektordner Systembefehle: `column`, `rename`,
`display` und `whatis`.
## Das Prinzip
Eine **Tabelle** ist eine Textdatei: Kopfzeile mit Spaltennamen, Strichzeile,
dann eine Zeile pro Datensatz, Spalten durch Tabulator getrennt. Jeder Befehl
liest eine Tabelle von stdin und schreibt eine Tabelle nach stdout. Befehle
lassen sich mit Pipes kombinieren.
```
cd demo
row 'Cost > 5' < inventory | column Description Cost | sorttable -nr Cost
compute 'Value = Cost * Amount' < inventory | total Value
jointable inventory owners
```
Dasselbe gilt für **Listen** (ein Datensatz als `Spalte<Tab>Wert`-Zeilen, die
Datei beginnt mit einer Leerzeile). `tabletolist` und `listtotable` wandeln um,
die meisten Befehle lesen beides.
`column -d Spalte ...` löscht Spalten (alle anderen bleiben), `column Spalte ...` wählt sie aus.
`row`, `compute` und `validate` übergeben ihren Ausdruck an awk, nachdem sie
Spaltennamen durch `$N` ersetzt haben. Es gilt also awk-Syntax.
Spaltennamen unterscheiden Groß- und Kleinschreibung. Mit `RDB_ICASE=1` wird
sie ignoriert (Details unter „Unterschiede zum Original“):
```
RDB_ICASE=1 compute 'wert = cost * amount' < inventory
```
## Hilfe
```
rdb alle Befehle
rdb man search die Original-Man-Page als Text (auch Themen: backup, dbase, dashline ...)
rdb --version Versionsnummer (Änderungen siehe CHANGELOG.md)
rdb man -g search dieselbe Seite mit groff formatiert (Beispiele sauber eingerückt)
whatis column Beschreibung und Syntax
whatwill total Befehle, die zu einem Stichwort passen
commands Tabelle aller Befehle
```
## Beispiele zu allen Befehlen
`demo/testall.cmds` enthält zu jedem Befehl ein Beispiel mit kurzer Erklärung.
`testall` führt es in einer Kopie von `demo/` aus und zeigt jede Zeile samt Ausgabe:
```
testall | less alle Beispiele ansehen
testall > testall.old Lauf als Vergleich festhalten
testall | diff testall.old - später prüfen, was sich geändert hat (mise run testall)
```
Eine Zeile mit `##` erscheint als Überschrift in der Ausgabe, eine mit `#` ist
ein stiller Kommentar. Neue Befehle brauchen hier ein Beispiel, sonst schlägt der Test
`test_testall_mentions_every_command` fehl.
## Beispiele aus dem gedruckten Handbuch
`doc/handbuch.txt` ist der Text des gedruckten Handbuchs „COHERENT /rdb Relational
Database Manager“ (Mark Williams, 346 Seiten), per OCR aus dem Scan `coherent.pdf`.
Er ist nicht korrigiert: Tabulatoren fehlen, Tabellen können verrutscht sein. Jede
Seite ist mit PDF- und Buchseite markiert (Buchseite = PDF-Seite − 10). Kapitel 1–16
sind ein Lehrbuch, Kapitel 17 enthält dieselben Man-Pages wie `man/`.
`demo/handbuch/` enthält die Beispiele aus dem Lehrbuch mit ihren Tabellen, ein
Unterordner je Kapitel. `beispiele.txt` hält zu jedem Beispiel den Befehl und die
Ausgabe, wie sie im Buch steht; `test/handbuch_test.rb` führt alles aus und vergleicht
(ohne Leerraum, das Buch richtet mit Leerzeichen aus). In der Datei bedeutet:
```
@ kap03 die folgenden Befehle laufen in diesem Unterordner
## S. 26 ... Überschrift mit Buchseite
$ befehl darunter die Ausgabe laut Buch
# ... Kommentar, z. B. ein Druckfehler des Buchs (die Ausgabe ist korrigiert)
! ... hier weichen wir bewusst ab; nicht verglichen, der Test meldet aber,
wenn die Ausgabe doch wieder zum Buch passt
```
## Vorlagen: `form` und `entry`
- `demo/form` ist eine Vorlage für `report` und `letter`: sie setzt Spaltenwerte in einen Text ein.
Sie hat keine Beschriftungen und nennt `Name` zweimal.
- `demo/entry` ist eine Eingabemaske für `screen`: jedes Feld kommt einmal vor und hat eine
Beschriftung. `screen < entry > maske.sh` erzeugt das Skript. Starten:
`CLEAR=$(clear.rdb) sh maske.sh > tabelle 2>&1`. Die Tabelle geht auf stderr (wie im Original),
daher das `2>&1`. Mit `form` würde die Maske leer und doppelt aussehen.
## Eingabemasken: `mask`
`mask` ist die Variante von `screen` mit eigener Oberfläche (nur Ruby, kein ncurses). Es zeichnet die
Vorlage (von der Standardeingabe, wie `screen`; ein Dateiname geht auch) selbst und schreibt die Eingabe als Tabelle auf die Standardausgabe, also pipefähig:
```
mask -b < demo/maskform Rahmen (-b), Felder, Schaltflächen
mask < demo/maskform > neu Tabelle in eine Datei
mask < demo/maskform | append -h kunden oder direkt in eine Tabelle
mask -s < demo/maskform nur das Bild und die Feldpositionen, ohne Terminal
```
In der Vorlage: `<Spalte>` oder `<Spalte:20>` ist ein Feld (mit Breite), `[ Speichern ]` eine Schaltfläche,
`[ Abbrechen ]` (oder Cancel, Abort, Quit) bricht ab, `<!date!>` fügt Befehlsausgabe ein. Kommt eine Spalte mehrfach
vor, ist es ein Feld. Tab/Pfeile wechseln, Enter bestätigt, Esc bricht ab (Status 1, keine Ausgabe).
## Zeilen auswählen: `pick`
`pick` zeigt eine Tabelle auf dem Terminal, man scrollt, filtert und wählt eine oder mehrere Zeilen. Die Tabelle
kommt von der Standardeingabe, die gewählten Zeilen gehen als Tabelle auf die Standardausgabe, also mitten in der Pipe:
```
pick < inventory eine Zeile wählen
pick -m < inventory | total Amount Value mehrere (Leertaste markiert, a alle)
column Item Description < inventory | pick -m | jointable - owners
pick -m -k 'down space down enter' < inventory Tasten aus einem String (Skripte, Tests)
```
Pfeile, Bild auf/ab, Pos1/Ende scrollen; `/` filtert (Enter behält den Filter, Esc löscht ihn); Enter wählt;
Esc bricht ab (Status 1, keine Ausgabe). Ohne Markierung gilt die Zeile unter dem Cursor.
## Steuertabellen 2026 für `tax`
`demo/` enthält vier Tarife für das Steuerjahr 2026 im Format von `tax` (income, tax, percent), jeweils für
Alleinstehende (Grundtarif). Erzeugt mit `ruby tools/gen_tax_tables.rb`.
| Datei | Land | Quelle | Genauigkeit |
|---|---|---|---|
| `taxDE2026` | Deutschland, Einkommensteuer | § 32a EStG (Grundfreibetrag 12 348 €) | Kurvenstücke durch Geraden genähert, Abweichung unter 0,15 € (die Formel rundet ab, `tax` auf) |
| `taxAT2026` | Österreich, Einkommensteuer | § 33 EStG (0 % bis 13 539 €, dann 20/30/40/48/50/55 %) | exakt, vor Absetzbeträgen |
| `taxCH2026` | Schweiz, **nur direkte Bundessteuer** | Art. 36 DBG, ESTV-Rundschreiben 2-215-D-2025 | Stufen exakt, das Gesetz rechnet in 100-Franken-Schritten |
| `taxLI2026` | Liechtenstein, **nur Landessteuer** | Art. 19 SteG (LR 640.0, Fassung 01.07.2026) | exakt |
| `taxCH_AI2026` | Kanton Appenzell I.Rh., einfache Staatssteuer | Art. 38 StG (ESTV-Kantonsblatt) | exakt |
| `taxCH_AR2026` | Kanton Appenzell A.Rh., einfache Staatssteuer (Alleinstehendentarif) | Art. 39 StG, Beträge ab 2024 | exakt |
| `taxCH_SG2026` | Kanton St. Gallen, einfache Staatssteuer | Art. 50 StG, Beträge ab 2026 | exakt |
| `taxCH_GR2026` | Kanton Graubünden, einfache Kantonssteuer | Art. 39 StG, indexierte Beträge ab 2025 | exakt |
```
tax 60000 < demo/taxDE2026 14233
tax 60000 < demo/taxAT2026 15447
tax 60000 < demo/taxCH2026 671
tax 60000 < demo/taxLI2026 1396 (mal 1 + Gemeindezuschlag, 150 % bis 250 %)
```
Der Betrag ist die Steuer auf das **zu versteuernde** Einkommen. Abzüge, Sozialversicherung, Absetzbeträge,
Kirchensteuer und Soli sind nicht enthalten. Für die Schweiz kommen Kanton und Gemeinde dazu (sie sind meist
höher als der Bund), für Liechtenstein der Gemeindezuschlag: gesamt = Landessteuer × (1 + Zuschlag/100).
Ehepaar- und Alleinerziehendentarife fehlen. Das ist keine Steuerberatung.
**Kantone (AI, AR, SG, GR):** Die Dateien tragen den Präfix `taxCH_` (Schweiz) vor dem Kantonskürzel; `taxCH2026` ist die Bundessteuer. Die Tabellen geben die *einfache Steuer* wieder. Sie wird mit dem Steuerfuss multipliziert,
und Gemeinde und Kirche kommen dazu. Der Kanton setzt den Steuerfuss pro Jahr fest:
| Kanton | Steuerfuss Kanton 2026 | Quelle |
|---|---|---|
| AI | 96 % | Kantonsblatt AI (Grossratsbeschluss 1.12.2025) |
| SG | 105 % | Budgetbeschluss des Kantonsrats (Websuche, nicht im Kantonsblatt) |
| GR | 92 % | Grosser Rat, Budget 2026 (Websuche) |
| AR | nicht ermittelt | AR rechnet in Steuereinheiten, die Zahl für 2026 habe ich nicht gefunden |
Beispiel AI bei 60 000: `tax 60000 < demo/taxCH_AI2026` = 3770, mal 0,96 = 3619 (Kantonssteuer, ohne Bezirk, Gemeinde und Kirche).
Oberhalb einer Grenze gilt bei allen vier Kantonen ein fester Satz auf das *ganze* Einkommen (AI ab 200 000 mit 8 %,
AR ab 260 800 mit 2,6 %, SG ab 264 500 mit 8,5 %, GR ab 780 440 mit 11 %); in der Tabelle ist das die letzte Zeile.
## Befehle
| Gruppe | Befehle |
|---|---|
| Auswahl | `row` `select` `column` `project` `sorttable` `record` `number` `Number` |
| Spalten | `compute` `addcol` `rename` |
| Mengen, Verbund | `jointable` `union` `intersect` `difference` `paste.rdb` `splittable` |
| Auswertung | `total` `mean` `maximum` `minimum` `subtotal` `width` `widest` `precision` `datatype` `schema` `dbdict` |
| Schnellzugriff | `index` `search` `seek` `append` `delete` `replace` `enter` `lock` `unlock` `vilock` `update.rdb` `ve` `vindex` |
| Prüfen | `check.rdb` `domain` `validate` `fd` `searchtree` `tableorlist` |
| Format | `justify` `trim` `compresss` `pad` `rmblank` `blank` `headon` `headoff` `insertdash` `display` `see` |
| Text | `cap` `uppercase` `lowercase` `substitute` `trimblank` `padstring` `length` `filesize` `ascii` `chr` `word` `prompt` `not` `path` |
| Umwandeln | `tabletolist` `listtotable` `listtosh` `tabletofix` `fixtotable` `tabletostruct` `tabletotbl` `tabletofact` `tabletorule` `tabletom4` `tabletosed` `translate` `uniondict` |
| Datum, Namen | `julian` `gregorian` `computedate` `todaysdate` `soundex` `like` `hashkey` |
| Berichte | `report` `reportwriter` `screen` `mask` `pick` `label` `letter` |
| Terminal, Hilfe | `termput` `tset` `clear.rdb` `cursor` `menu` `rdb` `commands` `helpme` `whatis` `whatwill` `howmany` `man` |
| Betrieb | `testall` `testsearch` `timesearch` `rmcore` `cpdir.rdb` |
| Buchhaltung (/act) | `act` `chartdup` `foot` `adjust` `balance` `close` `consolidate` `getjournal` `fillform` `calculate` `tax` `cashflow` `calcpay` `w2` `bom` `explode` `sale` `invoice` `cstate` `update.inv` |
## Schnellzugriff
Wie im Original sucht `search` über Byte-Offsets. Indizes bauen:
| Option | Methode | Datei |
|---|---|---|
| `-ms` | sequentiell (Vorgabe) | keine |
| `-mb` | binär: `index` sortiert die Tabelle selbst | keine |
| `-mr` | Datensatznummer | `tabelle.r` |
| `-mi` | invertiert (Schlüssel und Offset, sortiert) | `tabelle.i` |
| `-mh` | Hash, doppelt so viele Plätze wie Zeilen | `tabelle.h` |
```
index -mi inventory Description
echo scales | search -mi inventory Description
echo cl | search -mi -x inventory Description # Teiltreffer, Groß/klein egal
echo 5 | seek -mi inventory Item # von bis xvon xbis
```
`delete` überschreibt einen Satz mit Leerzeichen, ohne die Datei zu verschieben.
`replace` schreibt an dieselbe Stelle zurück und hängt den Satz hinten an, wenn
er nicht mehr passt (außer mit `-a`). Beide halten die Indizes aktuell.
`rmblank` räumt später auf.
## Unterschiede zum Original
- Es gibt kein COHERENT-Programmmaterial, daher sind die **Ausgaben nur anhand der
Man-Pages und dem gedruckten Handbuch geprüft**, nicht gegen das echte /rdb. Die
Beispiele aus den Man-Pages sind als Tests enthalten (`test/rdb_test.rb`), die aus
dem Lehrbuch in `demo/handbuch/` (`test/handbuch_test.rb`).
- Die Indexdateien sind dem Original nachgebaut, aber nicht gleich (Handbuch S. 103–108):
die Hashtabelle `.h` hat 2n statt 2n+1 Plätze, und in `.i` steht der Schlüssel vor
dem Offset statt dahinter. Alte /rdb-Indizes muss man neu bauen.
- `sorttable` lässt Zeilen mit gleichem Schlüssel in ihrer Reihenfolge; das Original
ordnet sie nach der ganzen Zeile, wie `sort` (Handbuch S. 94).
- `subtotal` schreibt bei einer Summe von null ein leeres Feld; das Handbuch zeigt
`0` und die Kontonummern als `101.0` (S. 30).
- Spaltennamen unterscheiden wie im Original Groß- und Kleinschreibung. Mit
`RDB_ICASE=1` (z. B. in `mise.toml` unter `[env]`) wird sie ignoriert:
ein exakter Treffer gewinnt, mehrere ähnliche sind ein Fehler. In
`row`/`compute`/`validate` bleiben awk-Wörter und Funktionsaufrufe unberührt
(eine Spalte `Length` verdeckt also nicht `length()`). Die Man-Pages von
`row`, `compute`, `column`, `sorttable`, `jointable`, `rename`, `validate` und
`rdb` haben dazu den Abschnitt „Case of Column Names“; er ist die einzige
Ergänzung gegenüber den Original-Seiten.
- `select` ist in bash, zsh und ksh ein reserviertes Wort. `select 'Cost > 5' < inventory` endet dort
mit `syntax error near unexpected token '<'`, weil die Shell das Schlüsselwort vor dem `PATH` auswertet.
Es gibt dasselbe unter dem Namen `row`. Außerdem gehen `\select ...`, `command select ...` und
`rdb select ...`.
- `jointable` braucht keine vorsortierten Tabellen.
- `ve` ist ein zeilenorientierter Editor, kein Bildschirmeditor.
- `soundex` nutzt den Standard-Algorithmus. Die Beispielwerte im Handbuch
weichen davon ab.
- Die /act-Befehle arbeiten auf Tabellen im aktuellen Ordner. Welche das sind
und welche Spalten sie brauchen, steht jeweils in der Man-Page. Das Paket ist
wie im Original nur ein Beispiel für Datenbankprogrammierung. Die Befehle, zu
denen das Handbuch nichts sagt (`post`, `purchase`, `ship` ...), fehlen.
## Aufbau
```
bin/rdb das eine Programm (die Symlinks darauf legt `rdb install` an, nicht im Repo)
lib/rdb.rb Registry der Befehle
lib/rdb/table.rb Tabellen und Listen lesen und schreiben
lib/rdb/fast.rb Schnellzugriff über Byte-Offsets
lib/rdb/cmd/*.rb die Befehle, nach Gruppen
man/ Man-Pages (troff-Quellen aus den MWC-Archiven)
demo/ Beispieltabellen; testall.cmds = je ein Beispiel pro Befehl, testall.old = letzter Lauf
demo/handbuch/ die Beispiele aus dem gedruckten Handbuch, je Kapitel ein Ordner
doc/handbuch.txt Text des gedruckten Handbuchs (OCR)
test/ Tests
```
Die Man-Pages und die Beispieltabellen stammen aus den MWC-Quellen. Lizenz der
Originale: BSD, Copyright 1977-1995 Robert Swartz.