303 lines
16 KiB
Markdown
303 lines
16 KiB
Markdown
# 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 # 132 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 (Hashfunktion FNV-1a), und in `.i` steht der
|
||
Schlüssel vor dem Offset statt dahinter. Alte /rdb-Indizes und `.h`-Dateien früherer
|
||
Versionen dieses Nachbaus muss man neu bauen.
|
||
- Summen, Mittelwerte und /act-Beträge sind Dezimalzahlen (`Rational`), gerundet wird halb auf.
|
||
- Dateien, die kein UTF-8 sind, liest `rdb` als Latin-1 und schreibt sie als UTF-8. Fehler
|
||
erscheinen als eine Zeile; mit `RDB_DEBUG=1` kommt der Ruby-Backtrace. Werte mit Tab oder
|
||
Zeilenumbruch lehnen `append`, `enter`, `replace` und `ve` ab.
|
||
- `lock`/`unlock` legen ihre Sperrdateien in `/tmp/rdb-locks-<UID>/` ab (Modus 0700, `flock`),
|
||
nicht als `/tmp/L<Tabelle>` wie in den Man-Pages. Eine Sperre eines abgestürzten Prozesses
|
||
wird nicht erkannt; `unlock` hebt sie auf.
|
||
- `vilock` sperrt atomar über einen Hardlink; `EDITOR` darf Argumente haben (`nvim -f`).
|
||
Nach `kill -9` bleibt `LOCK<Tabelle>` liegen und muss von Hand zurückbenannt werden.
|
||
- `report`: Spaltenwerte in `<!befehl <Spalte>!>` werden für die Shell maskiert und kommen
|
||
als ein Wort an; nicht in Anführungszeichen setzen. `screen` verlangt Spaltennamen, die
|
||
gültige Shell-Variablennamen sind (Buchstaben, Ziffern, `_`).
|
||
- `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.
|