ISAM-Datenbankunterstuetzung

Setzt den OpenSpec-Change phase-3-isam um (49/49 Aufgaben) und schliesst
damit Phase 3 ab. Alle 22 ISAM-Elemente des Inventars sind implementiert;
der Abdeckungsstand steigt auf 217 implementiert / 15 offen / 53
Non-Feature.

Frontend
- ISAM-Anweisungen als eigener Zweig im Parser: NAME [#]n [, arg ...],
  Sonderform ROLLBACK ALL ueber eine Sentinel-Kennung
- 22 Signaturen in builtin_stmt/builtin_fn, neue Argumentart R
  (Satzvariable eines benutzerdefinierten Typs)
- Satzargumente von INSERT/RETRIEVE/UPDATE werden gegen den Typ der
  Dateinummer geprueft, sofern beide literal bekannt sind
- Die Unsupported-Absenkung von OPEN ... FOR ISAM entfaellt

Speicherschicht (tb-runtime::isam, einziger Ort mit redb)
- Je Tabelle eine Satztabelle satz-id -> Satzbytes, je Index eine Tabelle
  Schluesselbytes -> satz-id. Satz-IDs sind monoton und werden nie
  wiederverwendet: der Cursor merkt sich eine ID, eine neu vergebene
  koennte still auf einen fremden Satz zeigen
- Satzbytes entstehen mit fileio::wert_schreiben/wert_lesen, also mit den
  Recordpuffern und der UTF-32-Festtextkodierung aus datei-eio
- Ordnungserhaltende Schluesselkodierung je Spaltentyp; Text als UTF-8,
  dessen Bytereihenfolge die Codepoint-Reihenfolge ist und damit dieselbe
  Ordnung wie CmpStr der VM. Ein Eigenschaftstest ueber 5000 zufaellige
  Wertepaare je Typ haelt fest, dass der Bytevergleich dem fachlichen
  Vergleich entspricht -- eine Ordnungsverletzung faellt sonst erst bei
  bestimmten Datenwerten auf
- Cursor als Wert (aktiver Index, letzte Satz-ID, unpositioniert), nicht
  als gehaltener Iterator: er ueberlebt Satzaenderungen und
  Transaktionsgrenzen
- Sicherungspunkte ueber ein eigenes Ruecknahmeprotokoll; ROLLBACK ALL
  bricht die Bibliothekstransaktion direkt ab

Semantik durchweg aus der Original-Hilfe
- Die Argumentformen stammen erstmals aus den Einzelseiten, nicht nur aus
  der Themenliste. Das korrigierte eine Annahme des Entwurfs: die
  Spaltenliste von CREATEINDEX ist keine Zeichenkette mit Trennzeichen,
  sondern eine Folge einzelner Stringargumente
- SEEKEQ mit unvollstaendigem Schluessel schlaegt immer fehl, SEEKGT mit
  Teilschluessel positioniert wie SEEKGE
- Nach SETINDEX ist der erste Satz der neuen Ordnung aktuell, nach DELETE
  der folgende
- Suche ueber den NULL-Index meldet 87, nicht 83; damit hat jeder Code von
  81 bis 89 einen Ausloeser
- Einzige Erweiterung: ein - vor dem Spaltennamen ordnet absteigend. Die
  Original-Hilfe kennt bei CREATEINDEX keine Sortierrichtung, die
  Anforderung verlangt sie. Kollisionsfrei, weil ein TYPE-Feldname nie mit
  - beginnen kann

CLOSE beendet keine Transaktion
- Erst schrieb es sie fest, womit das Schliessen irgendeiner Dateinummer
  die Transaktion aller anderen mit beendete und ein folgendes ROLLBACK
  ALL ins Leere lief. Ausstehende Aenderungen sind ohnehin festgeschrieben,
  weil jede Operation ausserhalb einer Transaktion fuer sich eine ist
- Ueber das Ende entscheiden allein COMMITTRANS und ROLLBACK ALL; eine
  beim Programmende offene Transaktion verfaellt, in beiden Wegen gleich

SETMEM und Fehler 89
- Ohne DOS-Speichermodell ist SETMEM die Obergrenze des ISAM-Puffers
  (Vorgabe 65536 Bytes); ohne echte Grenze waere Code 89 nie erreichbar
  und die Anweisung eine Attrappe

Tests
- 18 Einheitentests (Kodierung, Formatversion, Satz-IDs, Roundtrip ueber
  alle Feldtypen, Indexordnung, Cursor, Protokoll)
- 6 Korpusprogramme: Tabellen, Indizes, Cursor, Saetze, Transaktionen,
  Puffer und Vergleichsordnung -- je in einem temporaeren Arbeits-
  verzeichnis, der Projektbaum bleibt sauber
- Der VM-Test zu Fehler 73 prueft ISAM nicht mehr, sondern SETUEVENT;
  dafuer belegt ein neuer Test, dass OPEN ... FOR ISAM arbeitet

Dokumentation: Datenbankdateiformat in dateiformate.md, Abschnitt 10a
samt Sortierordnung und SETMEM-Abweichung in sprachreferenz.md,
ISAM-Abschnitt in bibliothek.md, Inventar auf implementiert, PLAN.md
schaerft die Leitplanke Referenzverhalten -- sie gilt auch gegenueber den
eigenen Spezifikationen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 11:37:55 +02:00
parent da114d86e9
commit 9b58e0ec43
30 changed files with 3615 additions and 50 deletions

10
Cargo.lock generated
View File

@@ -423,6 +423,15 @@ dependencies = [
"unicode-width 0.2.0", "unicode-width 0.2.0",
] ]
[[package]]
name = "redb"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de6c3b63e007e90ce536ec2ae4690826136a20ec8dbbbb400daef1bb999d2e36"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -614,6 +623,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"jiff", "jiff",
"log", "log",
"redb",
"thiserror", "thiserror",
"unicode-width 0.2.0", "unicode-width 0.2.0",
] ]

52
PLAN.md
View File

@@ -22,6 +22,12 @@ Konvention: `[ ]` offen · `[x]` erledigt · `[~]` in Arbeit
Formatierung), nicht das, was „richtiger" wäre. Wo das Vorbild aber mit der Formatierung), nicht das, was „richtiger" wäre. Wo das Vorbild aber mit der
heutigen Plattform kollidiert (Codepages, DOS-Hardware, Segmente), gewinnt heutigen Plattform kollidiert (Codepages, DOS-Hardware, Segmente), gewinnt
die Plattform; solche Abweichungen werden in der Sprachreferenz dokumentiert. die Plattform; solche Abweichungen werden in der Sprachreferenz dokumentiert.
**Das gilt auch gegenüber den eigenen Spezifikationen** (2026-09-04): Weicht
eine Delta-Spec bei Semantik oder Syntax vom Vorbild ab, ohne dass eine
Plattformkollision sie dazu zwingt, wird die **Spec** angeglichen, nicht das
Vorbild nachgebildet. Befund aus `phase-3-isam`, wo drei Festlegungen
(Cursorlage nach `SETINDEX` und nach `DELETE`, Fehlercode der Suche über den
NULL-Index) auf die Original-Hilfe zurückgeführt wurden.
- **Vollständigkeit ist das Soll** (2026-09-02): Erwartet wird eine - **Vollständigkeit ist das Soll** (2026-09-02): Erwartet wird eine
**100 % kompatible Sprachimplementierung und volle Standardbibliothek** **100 % kompatible Sprachimplementierung und volle Standardbibliothek**
des Vorbilds — abzüglich ausschließlich der in der Sprachreferenz des Vorbilds — abzüglich ausschließlich der in der Sprachreferenz
@@ -247,8 +253,8 @@ Druckzonen sowie die Korpus-Stringfunktionen (`tb-runtime`).
## Phase 3 — Laufzeitbibliothek (`tb-runtime`) und Bildschirm ## Phase 3 — Laufzeitbibliothek (`tb-runtime`) und Bildschirm
**Status: Kern abgeschlossen (2026-09-04), ISAM offen.** Umgesetzt im **Status: abgeschlossen (2026-09-04).** Umgesetzt in den OpenSpec-Changes
OpenSpec-Change `phase-3-runtime-bildschirm`; der Rest steckt im Change `phase-3-runtime-bildschirm` (Kern), `phase-3-ortszeit` und
`phase-3-isam`, mit dem Phase 3 endet. `phase-3-isam`, mit dem Phase 3 endet.
Ziel ist die **vollständige** Standardbibliothek des Vorbilds (siehe Ziel ist die **vollständige** Standardbibliothek des Vorbilds (siehe
@@ -260,9 +266,9 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
`qb45advr.hlp` mit Status und Fundstelle. Ein Test `qb45advr.hlp` mit Status und Fundstelle. Ein Test
(`crates/tb-frontend/tests/inventar.rs`) hält die Tabelle in beide (`crates/tb-frontend/tests/inventar.rs`) hält die Tabelle in beide
Richtungen gegen den Code — der Abdeckungsstand kann nicht mehr Richtungen gegen den Code — der Abdeckungsstand kann nicht mehr
veralten. **Stand: 195 implementiert · 37 offen · 53 Non-Feature.** veralten. **Stand: 217 implementiert · 15 offen · 53 Non-Feature.**
Die 37 offenen sind 22 ISAM (`phase-3-isam`), 13 Forms und Die 15 offenen sind 13 Forms und Ereignissteuerung (Phase 4) sowie
Ereignissteuerung (Phase 4), `RUN` und `$INCLUDE` (Phase 5). `RUN` und `$INCLUDE` (Phase 5).
- [x] **Bildschirmpuffer in die Laufzeit** (Entscheidung 2026-09-03): - [x] **Bildschirmpuffer in die Laufzeit** (Entscheidung 2026-09-03):
`Cell`/`TextScreen` liegen ratatui-frei in `tb-runtime::screen`; `Cell`/`TextScreen` liegen ratatui-frei in `tb-runtime::screen`;
`tb-ui` behält Farbabbildung, Widget und den Terminal-Host. Damit `tb-ui` behält Farbabbildung, Widget und den Terminal-Host. Damit
@@ -327,9 +333,14 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
alle 22 Korpusprogramme laufen ohne Laufzeitfehler 73; der Fehler alle 22 Korpusprogramme laufen ohne Laufzeitfehler 73; der Fehler
tritt nur noch bei bewusst offenen Elementen auf (ISAM, Forms, tritt nur noch bei bewusst offenen Elementen auf (ISAM, Forms,
Ereignissteuerung). Ereignissteuerung).
- [ ] **ISAM** (Change `phase-3-isam`): `OPEN … FOR ISAM`, Indizes, - [x] **ISAM** (Change `phase-3-isam`): `OPEN … FOR ISAM`, Indizes
Cursor, Satzoperationen, Transaktionen. Speicherschicht `redb` (ein- und mehrspaltig, eindeutig/mehrdeutig, auf-/absteigend),
(Entscheidung 2026-09-03). Damit endet Phase 3. Cursor mit `MOVE`- und `SEEK`-Familie, Satzoperationen,
Transaktionen mit Sicherungspunkten und die Fehler 8189.
Speicherschicht `redb` (Entscheidung 2026-09-03), ISAM-Semantik
und ordnungserhaltende Schlüsselkodierung in `tb-runtime::isam`.
Alle 22 ISAM-Elemente des Inventars sind damit implementiert.
**Damit endet Phase 3.**
**Befunde dieser Phase, die den Plan korrigiert haben** **Befunde dieser Phase, die den Plan korrigiert haben**
@@ -353,8 +364,29 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
Kalenderrechnung bleibt Eigenimplementierung. Kalenderrechnung bleibt Eigenimplementierung.
- `LPRINT` schreibt am Programmende nach `LPT1.TXT` im Arbeitsverzeichnis; - `LPRINT` schreibt am Programmende nach `LPT1.TXT` im Arbeitsverzeichnis;
einen plattformübergreifenden Druckerkanal gibt es nicht. einen plattformübergreifenden Druckerkanal gibt es nicht.
- Neue Abhängigkeiten: `unicode-width` (Doppelzellen) und `jiff` - Neue Abhängigkeiten: `unicode-width` (Doppelzellen), `jiff`
(Zeitzonenversatz). Für ISAM kommt `redb` hinzu. (Zeitzonenversatz) und `redb` (ISAM-Speicherschicht, auf
`tb-runtime::isam` beschränkt).
- Die Original-Hilfe kennt bei `CREATEINDEX` **keine** Sortierrichtung;
absteigende Indexspalten sind die einzige dokumentierte Erweiterung
(`-` vor dem Spaltennamen), festgehalten in der Sprachreferenz. Die
Cursor-Semantik folgt sonst durchweg dem Vorbild: die Delta-Spec hatte
nach `SETINDEX` und `DELETE` zunächst einen unpositionierten Cursor
vorgesehen und wurde am 2026-09-04 an die Original-Hilfe angeglichen
(siehe Leitplanke „Referenzverhalten schlägt Eleganz").
- Die Argumentformen der ISAM-Elemente stammen erstmals aus den
**Einzelseiten** der Original-Hilfe, nicht mehr nur aus der Themenliste.
Das korrigierte eine Annahme des Entwurfs: die Spaltenliste von
`CREATEINDEX` ist keine Zeichenkette mit Trennzeichen, sondern eine Folge
einzelner Stringargumente. Belegt in der Change-Notiz
`umfang-und-signaturen.md`; für spätere Phasen heißt das: die Themenliste
klärt den *Umfang*, die Einzelseiten klären die *Form*.
- `CLOSE` beendet **keine** laufende ISAM-Transaktion (Befund der
Verifikation, 2026-09-04). Anfangs schrieb es sie fest, womit das
Schließen irgendeiner Dateinummer die Transaktion aller anderen mit
beendete. Über das Ende einer Transaktion entscheiden allein
`COMMITTRANS` und `ROLLBACK ALL`; eine beim Programmende offene
Transaktion verfällt.
## Phase 4 — Forms-Engine (`tb-ui::forms`) ## Phase 4 — Forms-Engine (`tb-ui::forms`)
- [ ] Eingangsaufgabe (aus Phase 0 übernommen): Ereignisschleifen- - [ ] Eingangsaufgabe (aus Phase 0 übernommen): Ereignisschleifen-

View File

@@ -355,3 +355,36 @@ pub struct Module {
pub body: Vec<Stmt>, pub body: Vec<Stmt>,
pub procs: Vec<Proc>, pub procs: Vec<Proc>,
} }
/// Sentinel-Kennung für `ROLLBACK ALL`. Echte Sicherungspunkte zählen ab 1,
/// `0` bedeutet „letzter Sicherungspunkt bzw. Transaktionsbeginn" — der
/// negative Wert kann deshalb mit keiner Kennung kollidieren.
pub const ROLLBACK_ALL: i32 = -1;
/// ISAM-Anweisungen: Namen, die der Parser als Anweisung mit optionalem
/// `#` vor der Dateinummer liest. Die zugehörigen Funktionen (`GETINDEX$`,
/// `SAVEPOINT`, `SETMEM`, `BOF`) stehen nicht hier — sie sind gewöhnliche
/// Ausdrücke.
pub fn ist_isam_anweisung(name: &str) -> bool {
matches!(
name,
"CREATEINDEX"
| "DELETEINDEX"
| "SETINDEX"
| "INSERT"
| "RETRIEVE"
| "UPDATE"
| "DELETE"
| "DELETETABLE"
| "MOVEFIRST"
| "MOVELAST"
| "MOVENEXT"
| "MOVEPREVIOUS"
| "SEEKEQ"
| "SEEKGT"
| "SEEKGE"
| "BEGINTRANS"
| "COMMITTRANS"
| "ROLLBACK"
)
}

View File

@@ -330,6 +330,30 @@ pub enum Builtin {
Doevents, Doevents,
Sleep, Sleep,
Beep, Beep,
// ISAM (Change `phase-3-isam`)
IsamOpen,
IsamCreateIndex,
IsamDeleteIndex,
IsamSetIndex,
IsamGetIndexS,
IsamInsert,
IsamRetrieve,
IsamUpdate,
IsamDelete,
IsamDeleteTable,
IsamMoveFirst,
IsamMoveLast,
IsamMoveNext,
IsamMovePrevious,
IsamSeekEq,
IsamSeekGt,
IsamSeekGe,
IsamBeginTrans,
IsamCommitTrans,
IsamRollback,
IsamSavepoint,
IsamSetmem,
IsamBof,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -710,6 +710,20 @@ impl<'a> P<'a> {
} }
Some(Stmt::LockStmt { unlock, file, from, to, pos }) Some(Stmt::LockStmt { unlock, file, from, to, pos })
} }
// ISAM-Anweisungen: `NAME [#]n [, arg …]`. Sie unterscheiden sich
// von einem gewöhnlichen impliziten Aufruf nur durch das
// erlaubte `#` vor der Dateinummer; alles Weitere (Signatur,
// Absenkung) läuft über die Builtin-Anweisungstabelle.
// `=` und `(` schließen die Anweisungsdeutung aus: dann ist der
// Name eine Variable bzw. ein Array (`DELETE = 1`, `DELETE(2) = 1`).
// Keine ISAM-Syntaxform beginnt mit einer Klammer.
TokenKind::Ident { ref name, suffix: None }
if ist_isam_anweisung(name)
&& self.k_at(1) != TokenKind::Eq
&& self.k_at(1) != TokenKind::LParen =>
{
self.parse_isam(name.clone(), pos)
}
TokenKind::Ident { .. } => self.parse_assign_or_call(pos, false), TokenKind::Ident { .. } => self.parse_assign_or_call(pos, false),
_ => { _ => {
self.err("Syntax error"); self.err("Syntax error");
@@ -719,6 +733,31 @@ impl<'a> P<'a> {
} }
} }
/// ISAM-Anweisung: `NAME [#]dateinummer [, argument …]`.
/// Syntaxformen siehe `openspec/changes/phase-3-isam/umfang-und-signaturen.md`.
fn parse_isam(&mut self, name: String, pos: SourcePos) -> Option<Stmt> {
self.advance();
// `ROLLBACK ALL` — hier ist `ALL` Schlüsselwort, kein Bezeichner.
// Es senkt auf die Sentinel-Kennung ab, damit `ROLLBACK` eine
// einzige Signatur behält.
if name == "ROLLBACK" {
if let TokenKind::Ident { name: ref w, suffix: None } = self.k() {
if w == "ALL" {
self.advance();
let args = vec![Expr::LongLit(ROLLBACK_ALL)];
return Some(Stmt::Call { name, suffix: None, args, pos });
}
}
}
self.eat(&TokenKind::Hash);
let args = if self.at_stmt_end() {
Vec::new()
} else {
self.parse_arg_list_to_stmt_end()
};
Some(Stmt::Call { name, suffix: None, args, pos })
}
fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option<Stmt> { fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option<Stmt> {
self.advance(); // PRINT bzw. LPRINT self.advance(); // PRINT bzw. LPRINT
let file = if !printer && self.eat(&TokenKind::Hash) { let file = if !printer && self.eat(&TokenKind::Hash) {

View File

@@ -171,6 +171,7 @@ enum ArgK {
N, // numerisch N, // numerisch
S, // String S, // String
A, // beliebig A, // beliebig
R, // Satzvariable eines benutzerdefinierten Typs (ISAM)
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@@ -274,6 +275,11 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
"STACK" => (0, 0, &[], L), "STACK" => (0, 0, &[], L),
"ERDEV" => (0, 0, &[], I), "ERDEV" => (0, 0, &[], I),
"ERDEV$" => (0, 0, &[], St), "ERDEV$" => (0, 0, &[], St),
// ISAM-Funktionen (Formen siehe umfang-und-signaturen.md des Changes)
"GETINDEX$" => (1, 1, &[N], St),
"BOF" => (1, 1, &[N], I),
"SAVEPOINT" => (0, 0, &[], I),
"SETMEM" => (1, 1, &[N], L),
_ => return None, _ => return None,
}) })
} }
@@ -345,6 +351,27 @@ fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> {
"STACK" => (0, 1, &[N]), "STACK" => (0, 1, &[N]),
// `CALL SetFormatCC(49)` — Währungsformat nach Ländercode. // `CALL SetFormatCC(49)` — Währungsformat nach Ländercode.
"SETFORMATCC" => (1, 1, &[N]), "SETFORMATCC" => (1, 1, &[N]),
// ---- ISAM-Anweisungen -------------------------------------------
// Argumentformen wortgetreu aus der Original-Hilfe; belegt in
// openspec/changes/phase-3-isam/umfang-und-signaturen.md.
//
// CREATEINDEX ist variadisch: Dateinummer, Indexname,
// Eindeutigkeitskennzeichen, dann ein Stringargument je Spalte.
// Die Original-Hilfe nennt keine Obergrenze für die Spaltenzahl, also
// steht hier auch keine; die Argumenttypen ab dem vierten prüft
// `check_and_lower_builtin_args` gesondert.
"CREATEINDEX" => (4, u8::MAX, &[N, S, N, S]),
"DELETEINDEX" => (2, 2, &[N, S]),
// Ohne Indexnamen wird der NULL-Index (Einfügereihenfolge) gesetzt.
"SETINDEX" => (1, 2, &[N, S]),
"INSERT" | "RETRIEVE" | "UPDATE" => (2, 2, &[N, R]),
"DELETE" => (1, 1, &[N]),
"DELETETABLE" => (2, 2, &[S, S]),
"MOVEFIRST" | "MOVELAST" | "MOVENEXT" | "MOVEPREVIOUS" => (1, 1, &[N]),
// Dateinummer, dann ein Schlüsselwert je Indexspalte.
"SEEKEQ" | "SEEKGT" | "SEEKGE" => (2, 10, &[N, A, A, A, A, A, A, A, A, A]),
"BEGINTRANS" | "COMMITTRANS" => (0, 0, &[]),
"ROLLBACK" => (0, 1, &[N]),
_ => return None, _ => return None,
}) })
} }
@@ -377,6 +404,7 @@ pub fn lower(module: &Module) -> (Option<hir::HirModule>, Vec<Diagnostic>) {
data_marks_name: HashMap::new(), data_marks_name: HashMap::new(),
data_marks_line: HashMap::new(), data_marks_line: HashMap::new(),
hir_procs: Vec::new(), hir_procs: Vec::new(),
isam_typ: HashMap::new(),
}; };
let hir = s.run(module); let hir = s.run(module);
(hir, s.diags) (hir, s.diags)
@@ -394,6 +422,10 @@ struct Sema {
/// Benutzerdefinierte Typen: Name → Index, Definitionen geordnet. /// Benutzerdefinierte Typen: Name → Index, Definitionen geordnet.
udt_ids: HashMap<String, u16>, udt_ids: HashMap<String, u16>,
udt_defs: Vec<hir::HUdt>, udt_defs: Vec<hir::HUdt>,
/// Satztyp je ISAM-Dateinummer aus `OPEN … FOR ISAM typ tabelle AS #n`
/// mit literaler Nummer — Grundlage der Satztypprüfung von `INSERT`,
/// `RETRIEVE` und `UPDATE`.
isam_typ: HashMap<i32, String>,
/// Konstanten: Typ und gefalteter Wert. /// Konstanten: Typ und gefalteter Wert.
consts: HashMap<String, (Ty, Option<ConstVal>)>, consts: HashMap<String, (Ty, Option<ConstVal>)>,
/// Modulvariablen (Schlüssel wie `var_key`). /// Modulvariablen (Schlüssel wie `var_key`).
@@ -1512,12 +1544,40 @@ impl Sema {
// Vorgabe-Recordlänge des Vorbilds. // Vorgabe-Recordlänge des Vorbilds.
None => HExpr::Lng(128), None => HExpr::Lng(128),
}; };
if let Some((ty_name, _table)) = isam { if let Some((ty_name, table)) = isam {
if !ty_name.is_empty() && !self.udt_ids.contains_key(ty_name) { let Some(&udt) = self.udt_ids.get(ty_name) else {
self.err(*pos, "Type not defined"); self.err(*pos, "Type not defined");
return;
};
// Die Laufzeit braucht die Spaltennamen; `UdtLayout`
// führt nur die Feldtypen. Sie reisen deshalb als
// Stringargument mit — ohne Änderung am `.tbc`-Format.
let spalten = self.udt_defs[udt as usize]
.fields
.iter()
.map(|(n, _)| n.clone())
.collect::<Vec<_>>()
.join(",");
// Für die Satztypprüfung (Aufgabe 2.3) merken, welcher
// Typ an dieser Dateinummer hängt — nur bei literaler
// Nummer, sonst ist sie erst zur Laufzeit bekannt.
if let Some(ConstVal::Num(n)) = self.fold_const(number) {
self.isam_typ.insert(n as i32, ty_name.clone());
} }
// ISAM kommt mit dem Change `phase-3-isam`. self.push(
self.push(out, scope, HStmtKind::Unsupported("OPEN FOR ISAM")); out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::IsamOpen,
args: vec![
fe,
ne,
HExpr::Str(table.clone()),
HExpr::Str(spalten),
HExpr::Lng(udt as i32),
],
},
);
return; return;
} }
// Ohne `FOR`-Klausel gilt RANDOM (Vorbild). // Ohne `FOR`-Klausel gilt RANDOM (Vorbild).
@@ -2433,6 +2493,13 @@ impl Sema {
let a = self.lower_opt_args(args, &lowered); let a = self.lower_opt_args(args, &lowered);
self.push(out, scope, HStmtKind::BuiltinStmt { b, args: a }); self.push(out, scope, HStmtKind::BuiltinStmt { b, args: a });
} }
// ---- ISAM ------------------------------------------------------
"CREATEINDEX" | "DELETEINDEX" | "SETINDEX" | "INSERT" | "RETRIEVE"
| "UPDATE" | "DELETE" | "DELETETABLE" | "MOVEFIRST" | "MOVELAST"
| "MOVENEXT" | "MOVEPREVIOUS" | "SEEKEQ" | "SEEKGT" | "SEEKGE"
| "BEGINTRANS" | "COMMITTRANS" | "ROLLBACK" => {
self.lower_isam_stmt(name, args, lowered, scope, out)
}
// Bildschirm-/Datei-/System-Anweisungen späterer Phasen. // Bildschirm-/Datei-/System-Anweisungen späterer Phasen.
_ => self.push(out, scope, HStmtKind::Unsupported(match name { _ => self.push(out, scope, HStmtKind::Unsupported(match name {
"RUN" => "RUN", "RUN" => "RUN",
@@ -2443,6 +2510,68 @@ impl Sema {
} }
} }
/// ISAM-Anweisungen absenken. Die Dateinummer ist überall das erste
/// Argument (bei `DELETETABLE` stattdessen der Datenbankname); alle
/// weiteren Argumente reisen unverändert durch, weil die Laufzeit sie
/// gegen das Tabellenlayout prüfen muss.
#[allow(clippy::too_many_arguments)]
fn lower_isam_stmt(
&mut self,
name: &str,
args: &[Expr],
lowered: Vec<(HExpr, Ty)>,
scope: &mut Scope,
out: &mut Vec<HStmt>,
) {
let b = match name {
"CREATEINDEX" => Builtin::IsamCreateIndex,
"DELETEINDEX" => Builtin::IsamDeleteIndex,
"SETINDEX" => Builtin::IsamSetIndex,
"INSERT" => Builtin::IsamInsert,
"RETRIEVE" => Builtin::IsamRetrieve,
"UPDATE" => Builtin::IsamUpdate,
"DELETE" => Builtin::IsamDelete,
"DELETETABLE" => Builtin::IsamDeleteTable,
"MOVEFIRST" => Builtin::IsamMoveFirst,
"MOVELAST" => Builtin::IsamMoveLast,
"MOVENEXT" => Builtin::IsamMoveNext,
"MOVEPREVIOUS" => Builtin::IsamMovePrevious,
"SEEKEQ" => Builtin::IsamSeekEq,
"SEEKGT" => Builtin::IsamSeekGt,
"SEEKGE" => Builtin::IsamSeekGe,
"BEGINTRANS" => Builtin::IsamBeginTrans,
"COMMITTRANS" => Builtin::IsamCommitTrans,
_ => Builtin::IsamRollback,
};
// Satzargument gegen den Typ der Dateinummer prüfen, sofern beide
// literal bekannt sind (Aufgabe 2.3). Ist die Nummer erst zur
// Laufzeit bekannt, prüft die Laufzeit gegen das Tabellenlayout.
if matches!(name, "INSERT" | "RETRIEVE" | "UPDATE") {
if let (Some(ConstVal::Num(n)), Some((_, Ty::Udt(hat)))) =
(args.first().and_then(|a| self.fold_const(a)), lowered.get(1))
{
if let Some(soll) = self.isam_typ.get(&(n as i32)) {
if soll != hat {
self.err(args[1].pos(), "Type mismatch");
}
}
}
}
let mut hargs = Vec::with_capacity(lowered.len());
for (i, (e, t)) in lowered.into_iter().enumerate() {
// Erstes Argument ist die Dateinummer (bei DELETETABLE ein Name).
let ist_nummer = i == 0 && name != "DELETETABLE";
if ist_nummer || (name == "ROLLBACK" && i == 0) {
hargs.push(self.conv_num(e, &t, NumTy::Lng));
} else {
hargs.push(e);
}
}
self.push(out, scope, HStmtKind::BuiltinStmt { b, args: hargs });
}
/// Ausgabeziel von `PRINT` umschalten (1 Bildschirm, 2 Drucker, /// Ausgabeziel von `PRINT` umschalten (1 Bildschirm, 2 Drucker,
/// sonst Dateinummer). /// sonst Dateinummer).
fn push_ziel(&mut self, out: &mut Vec<HStmt>, scope: &mut Scope, n: HExpr) { fn push_ziel(&mut self, out: &mut Vec<HStmt>, scope: &mut Scope, n: HExpr) {
@@ -2497,6 +2626,8 @@ impl Sema {
} }
// Sonderfall INSTR([start%,] s$, such$) // Sonderfall INSTR([start%,] s$, such$)
let instr_with_start = name == "INSTR" && args.len() == 3; let instr_with_start = name == "INSTR" && args.len() == 3;
// Sonderfall CREATEINDEX: beliebig viele Spaltennamen ab Argument 4.
let createindex = name == "CREATEINDEX";
let mut out = Vec::new(); let mut out = Vec::new();
for (i, a) in args.iter().enumerate() { for (i, a) in args.iter().enumerate() {
if matches!(a, Expr::Missing) { if matches!(a, Expr::Missing) {
@@ -2509,7 +2640,7 @@ impl Sema {
0 => ArgK::N, 0 => ArgK::N,
_ => ArgK::S, _ => ArgK::S,
} }
} else if name == "INSTR" { } else if name == "INSTR" || (createindex && i >= 3) {
ArgK::S ArgK::S
} else { } else {
*spec.get(i).unwrap_or(&ArgK::A) *spec.get(i).unwrap_or(&ArgK::A)
@@ -2518,6 +2649,7 @@ impl Sema {
ArgK::N => is_num(&at), ArgK::N => is_num(&at),
ArgK::S => is_str(&at), ArgK::S => is_str(&at),
ArgK::A => true, ArgK::A => true,
ArgK::R => matches!(at, Ty::Udt(_) | Ty::Unknown),
}; };
if !ok { if !ok {
self.err(a.pos(), "Type mismatch"); self.err(a.pos(), "Type mismatch");
@@ -3369,6 +3501,20 @@ impl Sema {
} }
} }
"FREEFILE" => b(Builtin::Freefile, vec![], Ty::Int, self), "FREEFILE" => b(Builtin::Freefile, vec![], Ty::Int, self),
// --- ISAM-Funktionen ---
"GETINDEX$" => {
let f = conv_arg!(0, NumTy::Lng);
b(Builtin::IsamGetIndexS, vec![f], Ty::Str, self)
}
"BOF" => {
let f = conv_arg!(0, NumTy::Lng);
b(Builtin::IsamBof, vec![f], Ty::Int, self)
}
"SAVEPOINT" => b(Builtin::IsamSavepoint, vec![], Ty::Int, self),
"SETMEM" => {
let n = conv_arg!(0, NumTy::Lng);
b(Builtin::IsamSetmem, vec![n], Ty::Lng, self)
}
"ENVIRON$" => b(Builtin::EnvironS, vec![take(&lowered, 0).0], Ty::Str, self), "ENVIRON$" => b(Builtin::EnvironS, vec![take(&lowered, 0).0], Ty::Str, self),
"FRE" => b(Builtin::Fre, vec![], Ty::Lng, self), "FRE" => b(Builtin::Fre, vec![], Ty::Lng, self),
"STACK" => b(Builtin::StackFn, vec![], Ty::Lng, self), "STACK" => b(Builtin::StackFn, vec![], Ty::Lng, self),
@@ -3833,4 +3979,106 @@ mod tests {
// BYVAL (Klammern) konvertiert stattdessen // BYVAL (Klammern) konvertiert stattdessen
assert!(diags("SUB Foo (x&)\nEND SUB\nn% = 1\nFoo (n%)").is_empty()); assert!(diags("SUB Foo (x&)\nEND SUB\nn% = 1\nFoo (n%)").is_empty());
} }
// ---- ISAM (Change `phase-3-isam`) -----------------------------------
/// Satztyp und geöffnete Tabelle als Vorspann für die ISAM-Tests.
const ISAM_KOPF: &str = "TYPE KundeTyp\n Nummer AS LONG\n Name AS STRING * 20\n END TYPE\n DIM k AS KundeTyp\n OPEN \"db\" FOR ISAM KundeTyp \"Kunden\" AS #1\n";
/// Aufgabe 2.1: die Anweisung parst mit `#`-Dateinummer diagnose-frei.
#[test]
fn isam_anweisung_parst_ohne_diagnose() {
let d = diags(&format!("{ISAM_KOPF}SETINDEX #1, \"Name\""));
assert!(d.is_empty(), "SETINDEX soll diagnose-frei parsen: {d:?}");
// Auch ohne `#` (die Original-Hilfe schreibt es optional).
let d = diags(&format!("{ISAM_KOPF}SETINDEX 1, \"Name\""));
assert!(d.is_empty(), "{d:?}");
}
/// Aufgabe 2.2: falsche Argumentanzahl wird gemeldet und nennt das
/// Element (Guiding Principle).
#[test]
fn isam_argumentanzahl_nennt_das_element() {
let d = diags(&format!("{ISAM_KOPF}SEEKGT #1"));
assert!(
d.iter().any(|m| m.contains("SEEKGT")),
"Diagnose muss SEEKGT nennen: {d:?}"
);
assert!(
d.iter().any(|m| m.contains("Argument-count mismatch")),
"{d:?}"
);
}
/// Aufgabe 2.3: das Satzargument wird gegen den Tabellentyp geprüft.
#[test]
fn isam_satzargument_wird_typgeprueft() {
let d = diags(&format!("{ISAM_KOPF}DIM x AS STRING\nRETRIEVE #1, x$"));
assert!(
d.contains(&"Type mismatch".to_string()),
"String statt Satzvariable muss auffallen: {d:?}"
);
// Ein anderer benutzerdefinierter Typ passt ebenfalls nicht.
let quelle = format!(
"TYPE Anderer\n z AS INTEGER\nEND TYPE\n{ISAM_KOPF} DIM a AS Anderer\nRETRIEVE #1, a"
);
assert!(
diags(&quelle).contains(&"Type mismatch".to_string()),
"fremder Satztyp muss auffallen: {:?}",
diags(&quelle)
);
// Der richtige Typ ist diagnose-frei.
let d = diags(&format!("{ISAM_KOPF}RETRIEVE #1, k"));
assert!(d.is_empty(), "{d:?}");
}
/// Aufgabe 2.5: kein ISAM-Element endet als unbekannter Bezeichner —
/// ein Programm, das jedes Element genau einmal verwendet.
#[test]
fn kein_isam_element_ist_unbekannter_bezeichner() {
let quelle = format!(
"{ISAM_KOPF} CREATEINDEX #1, \"NachName\", 0, \"Name\"\n CREATEINDEX #1, \"Zwei\", 1, \"Name\", \"-Nummer\"\n SETINDEX #1, \"NachName\"\n i$ = GETINDEX$(1)\n INSERT #1, k\n MOVEFIRST #1\n MOVELAST #1\n MOVENEXT #1\n MOVEPREVIOUS #1\n SEEKEQ #1, \"a\"\n SEEKGT #1, \"a\"\n SEEKGE #1, \"a\"\n RETRIEVE #1, k\n UPDATE #1, k\n DELETE #1\n e% = EOF(1)\n b% = BOF(1)\n BEGINTRANS\n s% = SAVEPOINT\n ROLLBACK s%\n ROLLBACK\n ROLLBACK ALL\n COMMITTRANS\n m& = SETMEM(1024)\n DELETEINDEX #1, \"NachName\"\n CLOSE #1\n DELETETABLE \"db\", \"Kunden\"\n"
);
let d = diags(&quelle);
assert!(
!d.iter().any(|m| m.contains("not defined")),
"ISAM-Element wird als unbekannter Bezeichner abgewiesen: {d:?}"
);
assert!(d.is_empty(), "unerwartete Diagnosen: {d:?}");
}
/// `ROLLBACK ALL` senkt auf die Sentinel-Kennung ab und ist damit von
/// `ROLLBACK kennung` unterscheidbar.
#[test]
fn rollback_all_wird_als_sentinel_abgesenkt() {
let a = crate::analyze_source("TEST", "BEGINTRANS\nROLLBACK ALL");
assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
// `ALL` darf keine Variable werden — sonst zaehlte OPTION EXPLICIT es an.
let d = diags("OPTION EXPLICIT\nBEGINTRANS\nROLLBACK ALL");
assert!(d.is_empty(), "{d:?}");
}
/// `CREATEINDEX` ist in der Spaltenzahl unbegrenzt — die Original-Hilfe
/// nennt keine Obergrenze, also darf die Signatur auch keine setzen.
#[test]
fn createindex_ohne_obergrenze_der_spaltenzahl() {
let felder: String =
(1..=12).map(|i| format!(" F{i} AS LONG\n")).collect();
let spalten: String =
(1..=12).map(|i| format!(", \"F{i}\"")).collect();
let quelle = format!(
"TYPE Breit\n{felder}END TYPE\n DIM b AS Breit\n OPEN \"db\" FOR ISAM Breit \"Tab\" AS #1\n CREATEINDEX #1, \"Viel\", 0{spalten}"
);
let d = diags(&quelle);
assert!(d.is_empty(), "zwölfspaltiger Index muss zulässig sein: {d:?}");
// Ein Spaltenname muss trotzdem ein String sein.
let quelle = format!(
"TYPE Breit\n{felder}END TYPE\n DIM b AS Breit\n OPEN \"db\" FOR ISAM Breit \"Tab\" AS #1\n CREATEINDEX #1, \"Viel\", 0, \"F1\", 42"
);
assert!(
diags(&quelle).contains(&"Type mismatch".to_string()),
"Zahl als Spaltenname muss auffallen: {:?}",
diags(&quelle)
);
}
} }

View File

@@ -11,3 +11,4 @@ unicode-width.workspace = true
jiff.workspace = true jiff.workspace = true
thiserror.workspace = true thiserror.workspace = true
log.workspace = true log.workspace = true
redb = "4"

View File

@@ -25,6 +25,8 @@ pub struct RtState {
pub screen: TextScreen, pub screen: TextScreen,
/// Offene Dateien nach Dateinummer. /// Offene Dateien nach Dateinummer.
pub dateien: crate::fileio::Dateien, pub dateien: crate::fileio::Dateien,
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
pub isam: crate::isam::Isam,
pub print: PrintState, pub print: PrintState,
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form. /// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form.
pub tasten: VecDeque<String>, pub tasten: VecDeque<String>,
@@ -60,6 +62,7 @@ impl Default for RtState {
RtState { RtState {
screen: TextScreen::new(), screen: TextScreen::new(),
dateien: Default::default(), dateien: Default::default(),
isam: Default::default(),
print: PrintState::default(), print: PrintState::default(),
tasten: VecDeque::new(), tasten: VecDeque::new(),
abbruch: false, abbruch: false,
@@ -359,7 +362,31 @@ pub mod ids {
pub const STACK_STMT: u16 = 122; pub const STACK_STMT: u16 = 122;
pub const ERDEV: u16 = 123; pub const ERDEV: u16 = 123;
pub const ERDEV_S: u16 = 124; pub const ERDEV_S: u16 = 124;
pub const COUNT: u16 = 125; // ISAM (Change `phase-3-isam`)
pub const ISAM_OPEN: u16 = 125;
pub const ISAM_CREATE_INDEX: u16 = 126;
pub const ISAM_DELETE_INDEX: u16 = 127;
pub const ISAM_SET_INDEX: u16 = 128;
pub const ISAM_GET_INDEX_S: u16 = 129;
pub const ISAM_INSERT: u16 = 130;
pub const ISAM_RETRIEVE: u16 = 131;
pub const ISAM_UPDATE: u16 = 132;
pub const ISAM_DELETE: u16 = 133;
pub const ISAM_DELETE_TABLE: u16 = 134;
pub const ISAM_MOVE_FIRST: u16 = 135;
pub const ISAM_MOVE_LAST: u16 = 136;
pub const ISAM_MOVE_NEXT: u16 = 137;
pub const ISAM_MOVE_PREVIOUS: u16 = 138;
pub const ISAM_SEEK_EQ: u16 = 139;
pub const ISAM_SEEK_GT: u16 = 140;
pub const ISAM_SEEK_GE: u16 = 141;
pub const ISAM_BEGIN_TRANS: u16 = 142;
pub const ISAM_COMMIT_TRANS: u16 = 143;
pub const ISAM_ROLLBACK: u16 = 144;
pub const ISAM_SAVEPOINT: u16 = 145;
pub const ISAM_SETMEM: u16 = 146;
pub const ISAM_BOF: u16 = 147;
pub const COUNT: u16 = 148;
} }
/// Dispatch-Tabelle in Index-Reihenfolge. /// Dispatch-Tabelle in Index-Reihenfolge.
@@ -490,6 +517,30 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
bi_stack_stmt, bi_stack_stmt,
bi_erdev, bi_erdev,
bi_erdev_s, bi_erdev_s,
// ISAM
bi_isam_open,
bi_isam_create_index,
bi_isam_delete_index,
bi_isam_set_index,
bi_isam_get_index,
bi_isam_insert,
bi_isam_retrieve,
bi_isam_update,
bi_isam_delete,
bi_isam_delete_table,
bi_isam_move_first,
bi_isam_move_last,
bi_isam_move_next,
bi_isam_move_previous,
bi_isam_seek_eq,
bi_isam_seek_gt,
bi_isam_seek_ge,
bi_isam_begin_trans,
bi_isam_commit_trans,
bi_isam_rollback,
bi_isam_savepoint,
bi_isam_setmem,
bi_isam_bof,
]; ];
debug_assert_eq!(TABLE.len(), ids::COUNT as usize); debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
TABLE TABLE
@@ -951,10 +1002,15 @@ fn bi_open(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option
} }
fn bi_close(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_close(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
st.dateien.schliessen(arg_i32(a, 0)?).map(|_| None) let n = arg_i32(a, 0)?;
if st.isam.ist_isam(n) {
return st.isam.schliessen(n).map(|_| None);
}
st.dateien.schliessen(n).map(|_| None)
} }
fn bi_close_all(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_close_all(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
st.isam.alles_schliessen();
st.dateien.alle_schliessen().map(|_| None) st.dateien.alle_schliessen().map(|_| None)
} }
@@ -998,17 +1054,33 @@ fn bi_write_file(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<
} }
fn bi_eof(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_eof(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let e = st.dateien.get(arg_i32(a, 0)?)?.eof()?; let n = arg_i32(a, 0)?;
// Bei einer ISAM-Dateinummer meldet EOF das Ende der Tabelle in der
// Ordnung des aktiven Index, nicht das Dateiende.
if st.isam.ist_isam(n) {
return Ok(Some(Value::Int(if st.isam.eof(n) { -1 } else { 0 })));
}
let e = st.dateien.get(n)?.eof()?;
Ok(Some(Value::Int(if e { -1 } else { 0 }))) Ok(Some(Value::Int(if e { -1 } else { 0 })))
} }
fn bi_lof(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_lof(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let l = st.dateien.get(arg_i32(a, 0)?)?.laenge()?; let n = arg_i32(a, 0)?;
// ISAM: Zahl der Sätze in der Tabelle.
if st.isam.ist_isam(n) {
return Ok(Some(Value::Lng(st.isam.satzzahl(n)? as i32)));
}
let l = st.dateien.get(n)?.laenge()?;
Ok(Some(Value::Lng(l as i32))) Ok(Some(Value::Lng(l as i32)))
} }
fn bi_loc(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_loc(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let d = st.dateien.get(arg_i32(a, 0)?)?; let n = arg_i32(a, 0)?;
// ISAM: Kennung des aktuellen Satzes, 0 wenn unpositioniert.
if st.isam.ist_isam(n) {
return Ok(Some(Value::Lng(st.isam.satznummer(n)? as i32)));
}
let d = st.dateien.get(n)?;
// Position der letzten Operation (1-basiert): Datensatz bzw. Byte. // Position der letzten Operation (1-basiert): Datensatz bzw. Byte.
Ok(Some(Value::Lng(d.position.saturating_sub(1) as i32))) Ok(Some(Value::Lng(d.position.saturating_sub(1) as i32)))
} }
@@ -1587,6 +1659,169 @@ fn bi_beep(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<
Ok(None) Ok(None)
} }
// ---- ISAM -------------------------------------------------------------------
//
// Die Anweisungen legen ihre Argumente in der Reihenfolge der Original-Hilfe
// ab; die Dateinummer ist überall das erste (bei `DELETETABLE` der
// Datenbankname). Die Fachlogik steht in `crate::isam`.
fn bi_isam_open(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
// (datenbank$, nummer, tabelle$, spaltennamen$, udt-index)
let datenbank = arg_str(a, 0)?;
let nummer = arg_i32(a, 1)?;
let tabelle = arg_str(a, 2)?;
let spalten = arg_str(a, 3)?;
let udt = arg_i32(a, 4)? as u16;
st.isam.oeffnen(nummer, &datenbank, &tabelle, &spalten, udt).map(|_| None)
}
fn bi_isam_create_index(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
// (nummer, indexname$, eindeutig%, spalte$ …)
let nummer = arg_i32(a, 0)?;
let name = arg_str(a, 1)?;
let eindeutig = arg_i32(a, 2)? != 0;
let mut spalten = Vec::new();
for i in 3..a.len() {
spalten.push(arg_str(a, i)?.to_string());
}
st.isam.index_anlegen(nummer, &name, eindeutig, &spalten).map(|_| None)
}
fn bi_isam_delete_index(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let nummer = arg_i32(a, 0)?;
let name = arg_str(a, 1)?;
st.isam.index_loeschen(nummer, &name).map(|_| None)
}
fn bi_isam_set_index(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
// Ohne Indexnamen gilt der NULL-Index (Einfügereihenfolge).
let nummer = arg_i32(a, 0)?;
let name = if a.len() > 1 { arg_str(a, 1)?.to_string() } else { String::new() };
st.isam.index_setzen(nummer, &name).map(|_| None)
}
fn bi_isam_get_index(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
st.isam.index_name(n).map(|s| Some(Value::Str(Rc::from(s.as_str()))))
}
fn bi_isam_insert(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let nummer = arg_i32(a, 0)?;
let satz = a.get(1).cloned().ok_or(RuntimeError::TYPE_MISMATCH)?;
st.isam.einfuegen(nummer, &satz).map(|_| None)
}
fn bi_isam_retrieve(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
// Das Satzargument ist ein geteiltes Record-Handle: die gelesenen
// Feldwerte gehen direkt in die Variable des Programms.
let nummer = arg_i32(a, 0)?;
let ziel = match a.get(1) {
Some(Value::Rec(r)) => r.clone(),
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
let gelesen = st.isam.satz_lesen(nummer)?;
let Value::Rec(quelle) = gelesen else {
return Err(RuntimeError::TYPE_MISMATCH);
};
let werte = quelle.borrow().fields.clone();
let mut z = ziel.borrow_mut();
if z.fields.len() != werte.len() {
return Err(RuntimeError::TYPE_MISMATCH);
}
z.fields = werte;
Ok(None)
}
fn bi_isam_update(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let nummer = arg_i32(a, 0)?;
let satz = a.get(1).cloned().ok_or(RuntimeError::TYPE_MISMATCH)?;
st.isam.aendern(nummer, &satz).map(|_| None)
}
fn bi_isam_delete(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
st.isam.satz_loeschen(arg_i32(a, 0)?).map(|_| None)
}
fn bi_isam_delete_table(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let datenbank = arg_str(a, 0)?;
let tabelle = arg_str(a, 1)?;
st.isam.tabelle_loeschen(&datenbank, &tabelle).map(|_| None)
}
fn isam_bewegen(
st: &mut RtState,
a: &[Value],
richtung: crate::isam::Richtung,
) -> Result<Option<Value>, RuntimeError> {
st.isam.bewegen(arg_i32(a, 0)?, richtung).map(|_| None)
}
fn bi_isam_move_first(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
isam_bewegen(st, a, crate::isam::Richtung::Erster)
}
fn bi_isam_move_last(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
isam_bewegen(st, a, crate::isam::Richtung::Letzter)
}
fn bi_isam_move_next(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
isam_bewegen(st, a, crate::isam::Richtung::Naechster)
}
fn bi_isam_move_previous(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
isam_bewegen(st, a, crate::isam::Richtung::Voriger)
}
fn isam_suchen(
st: &mut RtState,
a: &[Value],
art: crate::isam::Suchart,
) -> Result<Option<Value>, RuntimeError> {
let nummer = arg_i32(a, 0)?;
st.isam.suchen(nummer, art, &a[1..]).map(|_| None)
}
fn bi_isam_seek_eq(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
isam_suchen(st, a, crate::isam::Suchart::Gleich)
}
fn bi_isam_seek_gt(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
isam_suchen(st, a, crate::isam::Suchart::Groesser)
}
fn bi_isam_seek_ge(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
isam_suchen(st, a, crate::isam::Suchart::GroesserGleich)
}
fn bi_isam_begin_trans(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
st.isam.trans_beginn().map(|_| None)
}
fn bi_isam_commit_trans(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
st.isam.trans_festschreiben().map(|_| None)
}
fn bi_isam_rollback(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
// Ohne Argument: bis zum letzten Sicherungspunkt bzw. Transaktionsbeginn.
let kennung = if a.is_empty() { 0 } else { arg_i32(a, 0)? };
st.isam.ruecknahme(kennung).map(|_| None)
}
fn bi_isam_savepoint(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
st.isam.sicherungspunkt().map(|k| Some(Value::Int(k)))
}
fn bi_isam_setmem(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let delta = arg_i32(a, 0)? as i64;
let frei = st.isam.setmem(delta);
Ok(Some(Value::Lng(frei.clamp(i32::MIN as i64, i32::MAX as i64) as i32)))
}
fn bi_isam_bof(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
Ok(Some(Value::Int(if st.isam.bof(n) { -1 } else { 0 })))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,439 @@
//! Einheitentests des ISAM-Moduls: Schlüsselkodierung, Speicherschicht,
//! Cursor und Rücknahmeprotokoll.
use super::*;
use crate::value::RecordObj;
use std::cell::RefCell;
use std::rc::Rc;
/// Kleiner Zähler-PRNG für den Eigenschaftstest — reproduzierbar und ohne
/// zusätzliche Abhängigkeit (xorshift32).
struct Zufall(u32);
impl Zufall {
fn next(&mut self) -> u32 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 17;
self.0 ^= self.0 << 5;
self.0
}
}
fn kodiere(w: &Value, t: &TypeInit, absteigend: bool) -> Vec<u8> {
let mut v = Vec::new();
schluessel_anhaengen(&mut v, w, t, absteigend);
v
}
// ---- Aufgabe 3.5: ordnungserhaltende Schlüsselkodierung --------------------
/// Kernanforderung aus D3: der Byte-Vergleich der Kodierung MUSS dem
/// fachlichen Vergleich entsprechen — sonst stehen Sätze in falscher
/// Reihenfolge, und zwar nur bei bestimmten Datenwerten.
#[test]
fn kodierung_erhaelt_die_ordnung_bei_zufallspaaren() {
let mut r = Zufall(0x1234_5678);
for _ in 0..5000 {
// INTEGER
let a = r.next() as i16;
let b = r.next() as i16;
assert_eq!(
kodiere(&Value::Int(a), &TypeInit::Int, false)
.cmp(&kodiere(&Value::Int(b), &TypeInit::Int, false)),
a.cmp(&b),
"INTEGER {a} vs {b}"
);
// LONG
let a = r.next() as i32;
let b = r.next() as i32;
assert_eq!(
kodiere(&Value::Lng(a), &TypeInit::Lng, false)
.cmp(&kodiere(&Value::Lng(b), &TypeInit::Lng, false)),
a.cmp(&b),
"LONG {a} vs {b}"
);
// CURRENCY (skalierte Ganzzahl)
let a = r.next() as i32 as i64;
let b = r.next() as i32 as i64;
assert_eq!(
kodiere(&Value::Cur(a), &TypeInit::Cur, false)
.cmp(&kodiere(&Value::Cur(b), &TypeInit::Cur, false)),
a.cmp(&b),
"CURRENCY {a} vs {b}"
);
// DOUBLE — endliche Werte über einen weiten Bereich
let a = (r.next() as i32 as f64) / 1024.0;
let b = (r.next() as i32 as f64) / 1024.0;
assert_eq!(
kodiere(&Value::Dbl(a), &TypeInit::Dbl, false)
.cmp(&kodiere(&Value::Dbl(b), &TypeInit::Dbl, false)),
a.partial_cmp(&b).unwrap(),
"DOUBLE {a} vs {b}"
);
// SINGLE
let a = (r.next() as i32 as f32) / 64.0;
let b = (r.next() as i32 as f32) / 64.0;
assert_eq!(
kodiere(&Value::Sng(a), &TypeInit::Sng, false)
.cmp(&kodiere(&Value::Sng(b), &TypeInit::Sng, false)),
a.partial_cmp(&b).unwrap(),
"SINGLE {a} vs {b}"
);
// Fester Text: die Kodierung ordnet wie der Stringvergleich des
// Dialekts, der auf UTF-8-Bytes vergleicht (interp::CmpStr).
let mach = |x: u32| -> String {
let zeichen = ['a', 'b', 'z', 'A', 'Z', 'ä', 'ß', '€'];
(0..4).map(|i| zeichen[((x >> (i * 3)) & 7) as usize]).collect()
};
let sa = mach(r.next());
let sb = mach(r.next());
let t = TypeInit::FixedStr(4);
assert_eq!(
kodiere(&Value::Str(Rc::from(sa.as_str())), &t, false)
.cmp(&kodiere(&Value::Str(Rc::from(sb.as_str())), &t, false)),
sa.as_bytes().cmp(sb.as_bytes()),
"TEXT {sa:?} vs {sb:?}"
);
}
}
#[test]
fn absteigende_spalte_kehrt_die_ordnung_um() {
let a = kodiere(&Value::Lng(10), &TypeInit::Lng, true);
let b = kodiere(&Value::Lng(20), &TypeInit::Lng, true);
assert!(a > b, "absteigend muss 10 hinter 20 einordnen");
}
/// Der Abschluss `00 00` sorgt dafür, dass ein echtes Präfix vor dem
/// längeren Text steht — auch als Teil eines mehrspaltigen Schlüssels.
#[test]
fn textabschluss_ordnet_praefix_vor_laengerem_text() {
let t = TypeInit::Str;
let kurz = kodiere(&Value::Str(Rc::from("ab")), &t, false);
let lang = kodiere(&Value::Str(Rc::from("abc")), &t, false);
assert!(kurz < lang);
// Auch absteigend bleibt die Umkehrung vollständig.
let kurz_ab = kodiere(&Value::Str(Rc::from("ab")), &t, true);
let lang_ab = kodiere(&Value::Str(Rc::from("abc")), &t, true);
assert!(kurz_ab > lang_ab);
}
#[test]
fn nullbyte_im_text_stoert_den_abschluss_nicht() {
let t = TypeInit::Str;
let a = kodiere(&Value::Str(Rc::from("a")), &t, false);
let b = kodiere(&Value::Str(Rc::from("a\u{0}b")), &t, false);
assert!(a < b, "\"a\" muss vor \"a\\0b\" stehen");
}
#[test]
fn praefix_ende_zaehlt_hoch() {
assert_eq!(praefix_ende(&[1, 2, 3]), Some(vec![1, 2, 4]));
assert_eq!(praefix_ende(&[1, 0xFF]), Some(vec![2]));
assert_eq!(praefix_ende(&[0xFF, 0xFF]), None);
}
// ---- Testaufbau für die Speicherschicht ------------------------------------
/// Satztyp der Tests: `Nummer AS LONG`, `Name AS STRING * 6`.
fn test_udts() -> Vec<UdtLayout> {
vec![UdtLayout {
name: "KUNDE".into(),
fields: vec![TypeInit::Lng, TypeInit::FixedStr(6)],
}]
}
fn satz(nummer: i32, name: &str) -> Value {
Value::Rec(Rc::new(RefCell::new(RecordObj {
fields: vec![Value::Lng(nummer), Value::Str(Rc::from(name))],
})))
}
struct TempDb {
dir: PathBuf,
}
impl TempDb {
fn neu(name: &str) -> TempDb {
let dir = std::env::temp_dir().join(format!(
"tb_isam_{name}_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
TempDb { dir }
}
fn pfad(&self, datei: &str) -> String {
self.dir.join(datei).to_string_lossy().to_string()
}
}
impl Drop for TempDb {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn isam_mit_tabelle(db: &str) -> Isam {
let mut i = Isam { udts: test_udts(), ..Default::default() };
i.oeffnen(1, db, "Kunden", "Nummer,Name", 0).unwrap();
i
}
// ---- Aufgabe 3.2: Formatversion --------------------------------------------
#[test]
fn hoehere_formatversion_wird_abgewiesen() {
let t = TempDb::neu("version");
let pfad = t.pfad("db.isam");
{
let mut i = isam_mit_tabelle(&pfad);
i.alles_schliessen();
}
// Version künstlich hochsetzen.
{
let db = Database::create(&pfad).unwrap();
let txn = db.begin_write().unwrap();
meta_schreiben(&txn, "version", &(FORMATVERSION + 1).to_le_bytes()).unwrap();
txn.commit().unwrap();
}
let mut i = Isam { udts: test_udts(), ..Default::default() };
let e = i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap_err();
assert_eq!(e, INKONSISTENT, "höhere Formatversion muss benannt scheitern");
}
#[test]
fn beschaedigte_datei_meldet_fehler_88() {
let t = TempDb::neu("kaputt");
let pfad = t.pfad("kaputt.isam");
std::fs::write(&pfad, b"das ist keine Datenbankdatei").unwrap();
let mut i = Isam { udts: test_udts(), ..Default::default() };
let e = i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap_err();
assert_eq!(e, RuntimeError(88));
}
// ---- Aufgabe 3.3: Satz-IDs werden nie wiederverwendet -----------------------
#[test]
fn geloeschte_satz_id_wird_nicht_neu_vergeben() {
let t = TempDb::neu("ids");
let mut i = isam_mit_tabelle(&t.pfad("db.isam"));
i.einfuegen(1, &satz(1, "eins")).unwrap();
let erste = i.satznummer(1).unwrap();
i.satz_loeschen(1).unwrap();
i.einfuegen(1, &satz(2, "zwei")).unwrap();
let zweite = i.satznummer(1).unwrap();
assert_ne!(erste, zweite, "eine gelöschte ID darf nicht neu vergeben werden");
assert!(zweite > erste);
}
// ---- Aufgabe 3.4: Satzserialisierung ---------------------------------------
#[test]
fn satz_roundtrip_ueber_alle_feldtypen() {
let t = TempDb::neu("roundtrip");
let udts = vec![UdtLayout {
name: "ALLES".into(),
fields: vec![
TypeInit::Int,
TypeInit::Lng,
TypeInit::Sng,
TypeInit::Dbl,
TypeInit::Cur,
TypeInit::FixedStr(5),
],
}];
let mut i = Isam { udts, ..Default::default() };
i.oeffnen(1, &t.pfad("db.isam"), "Alles", "A,B,C,D,E,F", 0).unwrap();
let original = Value::Rec(Rc::new(RefCell::new(RecordObj {
fields: vec![
Value::Int(-7),
Value::Lng(123456),
Value::Sng(1.5),
Value::Dbl(-2.25),
Value::Cur(98765),
Value::Str(Rc::from("äöü!")),
],
})));
i.einfuegen(1, &original).unwrap();
let Value::Rec(gelesen) = i.satz_lesen(1).unwrap() else { panic!("kein Record") };
let f = &gelesen.borrow().fields;
assert!(matches!(f[0], Value::Int(-7)));
assert!(matches!(f[1], Value::Lng(123456)));
assert!(matches!(f[2], Value::Sng(v) if v == 1.5));
assert!(matches!(f[3], Value::Dbl(v) if v == -2.25));
assert!(matches!(f[4], Value::Cur(98765)));
// Feste Strings kommen auf ihre Feldbreite aufgefüllt zurück.
assert!(matches!(&f[5], Value::Str(s) if &**s == "äöü! "));
}
// ---- Aufgabe 5.1: Indexeinträge stehen in Schlüsselreihenfolge -------------
#[test]
fn indexeintraege_stehen_in_schluesselreihenfolge() {
let t = TempDb::neu("indexordnung");
let mut i = isam_mit_tabelle(&t.pfad("db.isam"));
i.index_anlegen(1, "NachName", false, &["Name".into()]).unwrap();
for (n, name) in [(3, "Cäsar"), (1, "Anton"), (2, "Berta")] {
i.einfuegen(1, &satz(n, name)).unwrap();
}
i.index_setzen(1, "NachName").unwrap();
let mut namen = Vec::new();
i.bewegen(1, Richtung::Erster).unwrap();
while !i.eof(1) {
let Value::Rec(r) = i.satz_lesen(1).unwrap() else { panic!() };
let Value::Str(s) = r.borrow().fields[1].clone() else { panic!() };
namen.push(s.trim_end().to_string());
i.bewegen(1, Richtung::Naechster).unwrap();
}
assert_eq!(namen, vec!["Anton", "Berta", "Cäsar"]);
}
// ---- Aufgabe 6.1: Der Cursor überlebt eine Satzänderung --------------------
/// D5: Der Cursor ist ein Wert, kein gehaltener Iterator — er zeigt nach
/// einer Änderung an einem *anderen* Satz weiterhin auf denselben Satz.
#[test]
fn cursor_ueberlebt_eine_satzaenderung() {
let t = TempDb::neu("cursor");
let mut i = isam_mit_tabelle(&t.pfad("db.isam"));
i.index_anlegen(1, "NachName", false, &["Name".into()]).unwrap();
for (n, name) in [(1, "Anton"), (2, "Berta"), (3, "Cäsar")] {
i.einfuegen(1, &satz(n, name)).unwrap();
}
i.index_setzen(1, "NachName").unwrap();
i.bewegen(1, Richtung::Erster).unwrap();
i.bewegen(1, Richtung::Naechster).unwrap(); // steht auf Berta
let vorher = i.satznummer(1).unwrap();
// Über eine zweite Bindung einen anderen Satz einfügen.
i.oeffnen(2, &t.pfad("db.isam"), "Kunden", "Nummer,Name", 0).unwrap();
i.einfuegen(2, &satz(4, "Anna")).unwrap();
assert_eq!(i.satznummer(1).unwrap(), vorher, "Cursor darf nicht wandern");
let Value::Rec(r) = i.satz_lesen(1).unwrap() else { panic!() };
let Value::Str(s) = r.borrow().fields[1].clone() else { panic!() };
assert_eq!(s.trim_end(), "Berta");
// Und er bewegt sich weiter in der neuen Ordnung.
i.bewegen(1, Richtung::Naechster).unwrap();
let Value::Rec(r) = i.satz_lesen(1).unwrap() else { panic!() };
let Value::Str(s) = r.borrow().fields[1].clone() else { panic!() };
assert_eq!(s.trim_end(), "Cäsar");
}
// ---- Aufgabe 8.2: Rücknahmeprotokoll ---------------------------------------
/// Jede Satzoperation innerhalb einer Transaktion erzeugt genau einen
/// Protokolleintrag — die Grundlage der Sicherungspunkte (D6).
#[test]
fn jede_satzoperation_erzeugt_einen_protokolleintrag() {
let t = TempDb::neu("protokoll");
let mut i = isam_mit_tabelle(&t.pfad("db.isam"));
i.trans_beginn().unwrap();
assert_eq!(i.trans.as_ref().unwrap().log.len(), 0);
i.einfuegen(1, &satz(1, "eins")).unwrap();
assert_eq!(i.trans.as_ref().unwrap().log.len(), 1, "INSERT");
i.aendern(1, &satz(1, "EINS")).unwrap();
assert_eq!(i.trans.as_ref().unwrap().log.len(), 2, "UPDATE");
i.satz_loeschen(1).unwrap();
assert_eq!(i.trans.as_ref().unwrap().log.len(), 3, "DELETE");
i.trans_festschreiben().unwrap();
assert!(i.trans.is_none());
}
/// Außerhalb einer Transaktion gibt es nichts zurückzunehmen — das
/// Protokoll darf dann nicht wachsen.
#[test]
fn ohne_transaktion_kein_protokoll() {
let t = TempDb::neu("kein_protokoll");
let mut i = isam_mit_tabelle(&t.pfad("db.isam"));
i.einfuegen(1, &satz(1, "eins")).unwrap();
assert!(i.trans.is_none());
}
// ---- Namensprüfung (Fehler 81) ---------------------------------------------
#[test]
fn ungueltige_namen_werden_abgewiesen() {
assert!(name_pruefen("Kunden").is_ok());
assert!(name_pruefen("K_1").is_ok());
assert_eq!(name_pruefen("").unwrap_err(), NAME_UNGUELTIG);
assert_eq!(name_pruefen("1Kunde").unwrap_err(), NAME_UNGUELTIG);
assert_eq!(name_pruefen("Kun/den").unwrap_err(), NAME_UNGUELTIG);
assert_eq!(name_pruefen(&"x".repeat(32)).unwrap_err(), NAME_UNGUELTIG);
}
// ---- Befunde der Verifikation (2026-09-04) ---------------------------------
/// `CLOSE` beendet keine laufende Transaktion. Täte es das, schriebe das
/// Schließen irgendeiner Dateinummer die Änderungen aller anderen fest und
/// ein folgendes `ROLLBACK ALL` liefe ins Leere.
#[test]
fn close_beendet_keine_transaktion() {
let t = TempDb::neu("close_trans");
let mut i = isam_mit_tabelle(&t.pfad("db.isam"));
i.oeffnen(2, &t.pfad("db.isam"), "Kunden", "Nummer,Name", 0).unwrap();
i.trans_beginn().unwrap();
i.einfuegen(1, &satz(1, "eins")).unwrap();
i.schliessen(2).unwrap();
assert!(i.trans.is_some(), "CLOSE darf die Transaktion nicht beenden");
// Die Rücknahme wirkt danach noch auf die Änderung an #1.
i.ruecknahme(ROLLBACK_ALL).unwrap();
assert_eq!(i.satzzahl(1).unwrap(), 0);
}
/// Eine nicht festgeschriebene Transaktion verfällt beim Schließen aller
/// Dateien — dieselbe Lage wie am Programmende, wo sie fallen gelassen wird.
#[test]
fn nicht_festgeschriebene_transaktion_verfaellt() {
let t = TempDb::neu("verfall");
let pfad = t.pfad("db.isam");
{
let mut i = isam_mit_tabelle(&pfad);
i.trans_beginn().unwrap();
i.einfuegen(1, &satz(1, "eins")).unwrap();
i.alles_schliessen();
}
let mut i = Isam { udts: test_udts(), ..Default::default() };
i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap();
assert_eq!(
i.satzzahl(1).unwrap(),
0,
"eine offene Transaktion darf nicht stillschweigend wirken"
);
}
/// Scheitert `CREATEINDEX` an der Eindeutigkeit, bleibt keine halb gefüllte
/// Indextabelle zurück — auch nicht innerhalb einer Transaktion, wo der
/// Abbruch der Bibliothekstransaktion nicht aufräumen kann.
#[test]
fn gescheitertes_createindex_hinterlaesst_keinen_index() {
let t = TempDb::neu("kein_torso");
let mut i = isam_mit_tabelle(&t.pfad("db.isam"));
i.einfuegen(1, &satz(1, "gleich")).unwrap();
i.einfuegen(1, &satz(2, "gleich")).unwrap();
i.trans_beginn().unwrap();
let e = i.index_anlegen(1, "Eindeutig", true, &["Name".into()]).unwrap_err();
assert_eq!(e, DOPPELTER_SCHLUESSEL);
i.trans_festschreiben().unwrap();
assert_eq!(
i.index_setzen(1, "Eindeutig").unwrap_err(),
INDEX_FEHLT,
"der gescheiterte Index darf nicht als Torso zurückbleiben"
);
}

View File

@@ -8,6 +8,7 @@ pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe)
pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
pub mod isam; // ISAM: Tabellen, Indizes, Cursor, Transaktionen (redb)
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL
pub mod host; pub mod host;
pub mod screen; pub mod screen;

View File

@@ -1061,6 +1061,30 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::Doevents => ids::DOEVENTS, Builtin::Doevents => ids::DOEVENTS,
Builtin::Sleep => ids::SLEEP, Builtin::Sleep => ids::SLEEP,
Builtin::Beep => ids::BEEP, Builtin::Beep => ids::BEEP,
// ISAM
Builtin::IsamOpen => ids::ISAM_OPEN,
Builtin::IsamCreateIndex => ids::ISAM_CREATE_INDEX,
Builtin::IsamDeleteIndex => ids::ISAM_DELETE_INDEX,
Builtin::IsamSetIndex => ids::ISAM_SET_INDEX,
Builtin::IsamGetIndexS => ids::ISAM_GET_INDEX_S,
Builtin::IsamInsert => ids::ISAM_INSERT,
Builtin::IsamRetrieve => ids::ISAM_RETRIEVE,
Builtin::IsamUpdate => ids::ISAM_UPDATE,
Builtin::IsamDelete => ids::ISAM_DELETE,
Builtin::IsamDeleteTable => ids::ISAM_DELETE_TABLE,
Builtin::IsamMoveFirst => ids::ISAM_MOVE_FIRST,
Builtin::IsamMoveLast => ids::ISAM_MOVE_LAST,
Builtin::IsamMoveNext => ids::ISAM_MOVE_NEXT,
Builtin::IsamMovePrevious => ids::ISAM_MOVE_PREVIOUS,
Builtin::IsamSeekEq => ids::ISAM_SEEK_EQ,
Builtin::IsamSeekGt => ids::ISAM_SEEK_GT,
Builtin::IsamSeekGe => ids::ISAM_SEEK_GE,
Builtin::IsamBeginTrans => ids::ISAM_BEGIN_TRANS,
Builtin::IsamCommitTrans => ids::ISAM_COMMIT_TRANS,
Builtin::IsamRollback => ids::ISAM_ROLLBACK,
Builtin::IsamSavepoint => ids::ISAM_SAVEPOINT,
Builtin::IsamSetmem => ids::ISAM_SETMEM,
Builtin::IsamBof => ids::ISAM_BOF,
} }
} }

View File

@@ -106,6 +106,9 @@ impl Vm {
data_ptr: 0, data_ptr: 0,
module, module,
}; };
// ISAM leitet das Satzlayout aus dem Typ der `OPEN`-Anweisung ab und
// braucht dafür die UDT-Tabelle des Moduls.
vm.rt.isam.udts = vm.module.udts.clone();
vm.push_frame(0, 0); vm.push_frame(0, 0);
vm vm
} }

View File

@@ -34,6 +34,36 @@ fn err_code(src: &str) -> u16 {
} }
} }
/// Arbeitsverzeichnis für Tests mit Datei-E/A: ein frisches temporäres
/// Verzeichnis, das beim Verlassen samt Inhalt verschwindet. Programme
/// hinterlassen dadurch nichts im Projektbaum.
struct TempVerzeichnis {
vorher: std::path::PathBuf,
dir: std::path::PathBuf,
}
impl TempVerzeichnis {
fn neu(name: &str) -> TempVerzeichnis {
let vorher = std::env::current_dir().unwrap();
let dir = std::env::temp_dir().join(format!(
"tb_vm_{name}_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::env::set_current_dir(&dir).unwrap();
TempVerzeichnis { vorher, dir }
}
}
impl Drop for TempVerzeichnis {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.vorher);
let _ = std::fs::remove_dir_all(&self.dir);
}
}
// ---- 5.1 Ausdrücke und Konvertierungsmatrix -------------------------------- // ---- 5.1 Ausdrücke und Konvertierungsmatrix --------------------------------
#[test] #[test]
@@ -446,19 +476,34 @@ fn print_zonen_und_tab() {
#[test] #[test]
fn unsupported_feature_fehler_73() { fn unsupported_feature_fehler_73() {
// Dokumentiert, aber noch offen: ISAM kommt mit `phase-3-isam`, // Dokumentiert, aber noch offen: `SETUEVENT` kommt mit Phase 4
// die Ereignissteuerung mit Phase 4 → Laufzeitfehler 73. // → Laufzeitfehler 73 mit dem Katalogtext des Vorbilds (VBDOS).
let (ev, _) = run("TYPE T\n f AS INTEGER\nEND TYPE\nOPEN \"db\" FOR ISAM T \"tab\" AS #1"); let (ev, _) = run("SETUEVENT");
match ev { match ev {
RunEvent::Error { code, message, .. } => { RunEvent::Error { code, message, .. } => {
assert_eq!(code, 73); assert_eq!(code, 73);
// Katalogtext des Vorbilds (VBDOS) für Code 73:
assert_eq!(message, "Feature unavailable"); assert_eq!(message, "Feature unavailable");
} }
other => panic!("{other:?}"), other => panic!("{other:?}"),
} }
} }
/// Aufgabe 2.4 des Changes `phase-3-isam`: `OPEN … FOR ISAM` senkt nicht
/// mehr auf den „nicht verfügbar"-Marker ab, sondern arbeitet. Der Lauf
/// findet in einem temporären Verzeichnis statt und lässt nichts zurück.
#[test]
fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
let _dir = TempVerzeichnis::neu("open_isam");
let (ev, ausgabe) = run(
"TYPE T\n f AS INTEGER\nEND TYPE\n\
OPEN \"db.isam\" FOR ISAM T \"Tab\" AS #1\n\
PRINT \"offen\"\n\
CLOSE #1",
);
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
assert!(ausgabe.contains("offen"), "{ausgabe}");
}
// ---- Zustellpunkte des Hosts (Spec konsolen-basisbibliothek) --------------- // ---- Zustellpunkte des Hosts (Spec konsolen-basisbibliothek) ---------------
/// Vor blockierender Eingabe wird der Bildschirm angezeigt — sonst stünde /// Vor blockierender Eingabe wird der Bildschirm angezeigt — sonst stünde

View File

@@ -174,6 +174,75 @@ solange Werte übrig sind. Eine Zeichenkette ohne Feld bei vorhandenen
Werten löst Fehler 5 aus. `SetFormatCC` setzt das Währungszeichen Werten löst Fehler 5 aus. `SetFormatCC` setzt das Währungszeichen
über einen Ländercode; nicht aufgeführte Codes behalten `$`. über einen Ländercode; nicht aufgeführte Codes behalten `$`.
## ISAM
| Element | Art |
|---|---|
| `BEGINTRANS` | Anweisung |
| `BOF` | Funktion |
| `COMMITTRANS` | Anweisung |
| `CREATEINDEX` | Anweisung |
| `DELETE` | Anweisung |
| `DELETEINDEX` | Anweisung |
| `DELETETABLE` | Anweisung |
| `GETINDEX$` | Funktion |
| `INSERT` | Anweisung |
| `MOVEFIRST` | Anweisung |
| `MOVELAST` | Anweisung |
| `MOVENEXT` | Anweisung |
| `MOVEPREVIOUS` | Anweisung |
| `RETRIEVE` | Anweisung |
| `ROLLBACK` | Anweisung |
| `SAVEPOINT` | Funktion |
| `SEEKEQ` | Anweisung |
| `SEEKGE` | Anweisung |
| `SEEKGT` | Anweisung |
| `SETINDEX` | Anweisung |
| `SETMEM` | Funktion |
| `UPDATE` | Anweisung |
Indizierter Satzzugriff auf Tabellen einer Datenbankdatei; die
Dateinummer kommt aus `OPEN datenbank$ FOR ISAM typname tabelle$ AS #n`.
Bei allen Anweisungen ist das `#` vor der Dateinummer optional.
`CREATEINDEX #n, name$, eindeutig%, spalte$[, spalte$]…` legt einen Index
über bis zu neun Spalten an; `eindeutig%` ungleich 0 verbietet doppelte
Schlüssel (Verstoß: Fehler 86, Satzbestand bleibt unverändert). Ein `-`
vor dem Spaltennamen ordnet absteigend — eine Erweiterung über die
Original-Hilfe hinaus. `SETINDEX #n` ohne Namen (oder mit `""`) wählt den
NULL-Index (Einfügereihenfolge), `GETINDEX$` liefert dafür einen leeren
String. Nach `SETINDEX` ist der erste Satz der neuen Ordnung der aktuelle.
Unbekannter Index: Fehler 83, unbekannte Spalte: Fehler 84.
`INSERT`, `RETRIEVE` und `UPDATE` erwarten als zweites Argument eine
Variable des Tabellentyps, `DELETE` nur die Dateinummer. Ohne
positionierten Cursor melden `RETRIEVE`, `UPDATE` und `DELETE` Fehler 85.
Nach `DELETE` wird der folgende Satz der aktuelle; war der gelöschte der
letzte, steht der Cursor am Ende der Tabelle ohne aktuellen Satz. Alle
Indizes werden nach jeder Satzoperation nachgeführt.
Die `MOVE`-Familie bewegt in der Ordnung des aktiven Index, ohne aktiven
Index in Einfügereihenfolge. Läuft der Cursor über das Ende hinaus, wird
`EOF(n)` wahr, über den Anfang hinaus `BOF(n)`; in beiden Fällen ist der
Cursor danach unpositioniert. Die `SEEK`-Familie sucht im aktiven Index
(über den NULL-Index: Fehler 87); eine erfolglose Suche setzt `EOF`. Ein
mehrspaltiger Index lässt sich mit einem Präfix seiner Spalten
durchsuchen, wobei `SEEKEQ` mit unvollständigem Schlüssel immer fehlschlägt.
`BEGINTRANS` eröffnet eine Transaktion, `COMMITTRANS` schreibt sie fest.
`CLOSE` beendet sie nicht; eine beim Programmende offene Transaktion
bleibt wirkungslos.
`SAVEPOINT` liefert die Kennung eines Sicherungspunkts; `ROLLBACK kennung`
setzt auf ihn zurück und lässt die Transaktion offen, `ROLLBACK` ohne
Argument auf den letzten Sicherungspunkt bzw. den Transaktionsbeginn.
`ROLLBACK ALL` nimmt alles seit `BEGINTRANS` zurück und beendet die
Transaktion. Nach jeder Rücknahme ist der Cursor unpositioniert.
`SETMEM(n&)` verändert die ISAM-Puffergrenze um `n` Bytes und liefert die
danach verfügbare Größe (Vorgabe 65536); ein Überschreiten meldet Fehler
89. Bedeutung und Abweichung vom DOS-Speichermodell des Vorbilds stehen
in der Sprachreferenz.
## Klang ## Klang
| Element | Art | | Element | Art |

View File

@@ -69,6 +69,69 @@ Feste Strings (`STRING * n`) in Records werden als **UTF-32LE** gespeichert
des Vorbilds; numerische Felder behalten ihr klassisches Layout des Vorbilds; numerische Felder behalten ihr klassisches Layout
(INTEGER i16, LONG i32, SINGLE f32, DOUBLE f64, CURRENCY i64, little-endian). (INTEGER i16, LONG i32, SINGLE f32, DOUBLE f64, CURRENCY i64, little-endian).
## ISAM-Datenbank (`OPEN … FOR ISAM`)
Eigenes Format, **bewusst nicht binärkompatibel** zu Datenbankdateien des
Vorbilds — dieselbe Linie wie bei den UTF-32-Records oben. Ein
Konvertierungswerkzeug ist Nicht-Ziel; das Vorbildformat wird weder
gelesen noch geschrieben.
**Träger.** Die Datei ist eine [`redb`](https://crates.io/crates/redb)-Datei
(eingebetteter transaktionaler B-Baum). `redb` liefert Seitenverwaltung,
Transaktionen und Crash-Sicherheit; die ISAM-Semantik darüber ist eigener
Code (siehe `tb-runtime::isam`).
**Formatversion.** Die Datei trägt in ihrer Metatabelle unter `version`
eine 32-Bit-Formatversion (little-endian), derzeit **1**. Eine Datei mit
höherer Version wird beim Öffnen mit Laufzeitfehler 88 („ISAM - Database
inconsistent") abgewiesen, statt fehlinterpretiert zu werden. Ebenso
abgewiesen wird eine bestehende Datei ohne Versionsmarke sowie jede
strukturell unlesbare Datei.
**Tabellen in der Datei.** Je Datenbankdatei:
| `redb`-Tabelle | Schlüssel → Wert | Inhalt |
|---|---|---|
| `tb_meta` | `&str → Bytes` | Formatversion, Layout, Indexdefinitionen, ID-Zähler |
| `satz/<tabelle>` | `u64 → Bytes` | ein Satz je Eintrag, Schlüssel ist die Satz-ID |
| `idx/<tabelle>/<index>` | `Bytes → u64` | Indexeintrag: Schlüsselbytes → Satz-ID |
Die Einträge in `tb_meta` je Tabelle `T`:
- `tab:T` — Layoutkurzform `spalte:typ;…` mit den Typkürzeln `I2`, `I4`,
`R4`, `R8`, `CY`, `T<n>` (fester Text mit `n` Zeichen). Beim Öffnen einer
bestehenden Tabelle wird sie gegen den angegebenen Satztyp geprüft;
Abweichung ist Fehler 88.
- `idx:T` — eine Zeile je Index, `name<TAB>eindeutig<TAB>spalten`, Spalten
als Feldnummern mit `-` für absteigend.
- `seq:T` — nächste Satz-ID (u64, little-endian). IDs werden **monoton**
vergeben und **nie wiederverwendet**: ein Cursor merkt sich eine Satz-ID,
und eine nach `DELETE` neu vergebene ID ließe ihn still auf einen fremden
Satz zeigen.
**Satzbytes.** Ein Satz liegt genau so im Speicher wie ein Record bei
`PUT` auf eine `RANDOM`-Datei: numerische Felder klassisch und
little-endian, feste Strings als UTF-32LE. Daher gilt die Inkompatibilität
zum Vorbild aus demselben Grund wie oben.
**Indexschlüssel.** Die Schlüsselbytes sind **ordnungserhaltend** kodiert —
ihr Byte-Vergleich entspricht dem fachlichen Vergleich, weil `redb` nach
Bytes ordnet:
- Ganzzahlen: Big-Endian fester Breite mit gekipptem Vorzeichenbit.
- Gleitkommazahlen: Big-Endian der Bitdarstellung; bei negativem
Vorzeichen alle Bits invertiert, sonst das oberste Bit gesetzt.
- Text: UTF-8 (dessen Bytereihenfolge ist die Codepoint-Reihenfolge, siehe
„Sortierordnung" der Sprachreferenz). `00` im Text wird zu `00 FF`
verdoppelt, den Abschluss bildet `00 00` — damit ordnet ein echtes
Präfix vor jedem längeren Text.
- Absteigende Spalten: alle Bytes dieser Spalte invertiert.
- Mehrspaltig: Verkettung in Spaltenreihenfolge.
An jeden Indexschlüssel sind acht Bytes Satz-ID (Big-Endian) angehängt.
Das macht auch Einträge eines mehrdeutigen Index eindeutig, hält Dubletten
in Einfügereihenfolge und macht die Präfixsuche zur Bereichsabfrage.
## Kompilat: `.tbc` (neu, eigenes Format) ## Kompilat: `.tbc` (neu, eigenes Format)
Container für TBVM-Bytecode, Entwurf in [tbvm-design.md](tbvm-design.md). Container für TBVM-Bytecode, Entwurf in [tbvm-design.md](tbvm-design.md).

View File

@@ -21,7 +21,7 @@ namentlich abgewiesen. `Non-Feature` = abgelehnt, gelistet unter
**Fundstelle.** Bei `implementiert` das Modul, bei `Non-Feature` der **Fundstelle.** Bei `implementiert` das Modul, bei `Non-Feature` der
Abschnitt der Sprachreferenz, bei `offen` ein Strich. Abschnitt der Sprachreferenz, bei `offen` ein Strich.
**Abdeckung.** implementiert 195 · offen 37 · Non-Feature 53 · gesamt 285 **Abdeckung.** implementiert 217 · offen 15 · Non-Feature 53 · gesamt 285
| Name | Art | Gruppe | Status | Fundstelle | Quelle | | Name | Art | Gruppe | Status | Fundstelle | Quelle |
|---|---|---|---|---|---| |---|---|---|---|---|---|
@@ -199,28 +199,28 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `VARPTR$` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `VARPTR$` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `VARSEG` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `VARSEG` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `WAIT` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `WAIT` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `BEGINTRANS` | Anweisung | ISAM | offen | - | bas7advr | | `BEGINTRANS` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `BOF` | Funktion | ISAM | offen | - | bas7advr | | `BOF` | Funktion | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `COMMITTRANS` | Anweisung | ISAM | offen | - | bas7advr | | `COMMITTRANS` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `CREATEINDEX` | Anweisung | ISAM | offen | - | bas7advr | | `CREATEINDEX` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `DELETE` | Anweisung | ISAM | offen | - | bas7advr | | `DELETE` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `DELETEINDEX` | Anweisung | ISAM | offen | - | bas7advr | | `DELETEINDEX` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `DELETETABLE` | Anweisung | ISAM | offen | - | bas7advr | | `DELETETABLE` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `GETINDEX$` | Funktion | ISAM | offen | - | bas7advr | | `GETINDEX$` | Funktion | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `INSERT` | Anweisung | ISAM | offen | - | bas7advr | | `INSERT` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `MOVEFIRST` | Anweisung | ISAM | offen | - | bas7advr | | `MOVEFIRST` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `MOVELAST` | Anweisung | ISAM | offen | - | bas7advr | | `MOVELAST` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `MOVENEXT` | Anweisung | ISAM | offen | - | bas7advr | | `MOVENEXT` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `MOVEPREVIOUS` | Anweisung | ISAM | offen | - | bas7advr | | `MOVEPREVIOUS` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `RETRIEVE` | Anweisung | ISAM | offen | - | bas7advr | | `RETRIEVE` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `ROLLBACK` | Anweisung | ISAM | offen | - | bas7advr | | `ROLLBACK` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `SAVEPOINT` | Funktion | ISAM | offen | - | bas7advr | | `SAVEPOINT` | Funktion | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `SEEKEQ` | Anweisung | ISAM | offen | - | bas7advr | | `SEEKEQ` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `SEEKGE` | Anweisung | ISAM | offen | - | bas7advr | | `SEEKGE` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `SEEKGT` | Anweisung | ISAM | offen | - | bas7advr | | `SEEKGT` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `SETINDEX` | Anweisung | ISAM | offen | - | bas7advr | | `SETINDEX` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `SETMEM` | Funktion | ISAM | offen | - | bas7advr | | `SETMEM` | Funktion | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `UPDATE` | Anweisung | ISAM | offen | - | bas7advr | | `UPDATE` | Anweisung | ISAM | implementiert | tb-runtime::isam | bas7advr |
| `CALLS` | Anweisung | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `CALLS` | Anweisung | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `SADD` | Funktion | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `SADD` | Funktion | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `SSEG` | Funktion | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `SSEG` | Funktion | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |

View File

@@ -259,11 +259,87 @@ solange ein Menü-Control den Fokus hat. Für Terminal Basic relevant:
`LOC(n)`, `LOF(n)`. `LOC(n)`, `LOF(n)`.
- Verwaltung: `NAME alt$ AS neu$`, `KILL datei$`, `FILES [muster$]`, - Verwaltung: `NAME alt$ AS neu$`, `KILL datei$`, `FILES [muster$]`,
`CHDIR`, `MKDIR`, `RMDIR`, `FILEATTR`, `FREEFILE`. `CHDIR`, `MKDIR`, `RMDIR`, `FILEATTR`, `FREEFILE`.
- **ISAM** (Professional-Edition des Vorbilds): wird unterstützt
(Entscheidung 2026-09-02) — `OPEN … FOR ISAM`, Tabellen/Indizes,
`SEEKGT`-Familie usw.; Umfang wird mit dem Inventar (Phase 3) erfasst.
- Pfade: plattformneutral; `/` und `\` werden akzeptiert. - Pfade: plattformneutral; `/` und `\` werden akzeptiert.
## 10a. ISAM (indizierter Satzzugriff)
ISAM der Professional-Edition des Vorbilds wird unterstützt (Entscheidung
2026-09-02). Das Dateiformat beschreibt
[dateiformate.md](dateiformate.md#isam-datenbank-open--for-isam).
- `OPEN datenbank$ FOR ISAM typname tabellenname$ AS [#]n` bindet eine
Tabelle an eine Dateinummer; die Felder des benannten `TYPE` sind ihre
Spalten. Die Tabelle entsteht beim ersten Öffnen. `CLOSE` löst die
Bindung; jede Operation außerhalb einer Transaktion ist für sich schon
festgeschrieben. Eine laufende Transaktion lässt `CLOSE` unberührt —
über ihr Ende entscheiden `COMMITTRANS` und `ROLLBACK ALL`; eine beim
Programmende offene Transaktion bleibt wirkungslos.
- Indizes: `CREATEINDEX [#]n, indexname$, eindeutig%, spalte$[, spalte$]…`,
`DELETEINDEX [#]n, indexname$`, `SETINDEX [#]n[, indexname$]`,
`GETINDEX$(n)`. `SETINDEX` ohne Namen wählt den NULL-Index
(Einfügereihenfolge); `GETINDEX$` liefert dafür einen leeren String.
- Sätze: `INSERT`, `RETRIEVE`, `UPDATE` (je `[#]n, satzvariable`) und
`DELETE [#]n` auf dem aktuellen Satz.
- Cursor: `MOVEFIRST`, `MOVELAST`, `MOVENEXT`, `MOVEPREVIOUS` (je `[#]n`)
in der Ordnung des aktiven Index; `EOF(n)` und `BOF(n)` melden das
Überlaufen am Ende bzw. am Anfang.
- Suche: `SEEKEQ`, `SEEKGT`, `SEEKGE` (je `[#]n, schlüsselwert[, …]`).
Ein mehrspaltiger Index lässt sich mit einem Präfix seiner Spalten
durchsuchen; `SEEKEQ` mit unvollständigem Schlüssel schlägt dabei immer
fehl (wie im Vorbild), `SEEKGE` und `SEEKGT` positionieren auf dem ersten
passenden Satz. Über den NULL-Index lässt sich nicht suchen (Fehler 87).
- Tabellen: `DELETETABLE datenbank$, tabellenname$`.
- Transaktionen: `BEGINTRANS`, `COMMITTRANS`, `SAVEPOINT` (Funktion,
liefert die Kennung), `ROLLBACK [kennung]` und `ROLLBACK ALL`.
- `LOF(n)` liefert für eine ISAM-Dateinummer die Satzzahl der Tabelle,
`LOC(n)` die Kennung des aktuellen Satzes (0 = unpositioniert).
- Cursorlage wie im Vorbild: nach `SETINDEX` ist der erste Satz der neuen
Ordnung der aktuelle; nach `DELETE` der auf den gelöschten folgende, und
war der gelöschte der letzte, steht der Cursor am Ende der Tabelle ohne
aktuellen Satz.
- Fehlercodes 8189 (`ERR`): 81 ungültiger Name, 82 Tabelle nicht
gefunden, 83 Index nicht gefunden, 84 ungültige Spalte, 85 kein aktueller
Satz, 86 Verstoß gegen einen eindeutigen Index, 87 unzulässige Operation
auf dem NULL-Index (Suche), 88 Datenbank inkonsistent, 89 ISAM-Puffer zu
klein.
**Sortierordnung der Indizes.** Textspalten ordnen nach
**Unicode-Codepoints**; Groß- und Kleinschreibung werden **unterschieden**.
Es ist dieselbe Ordnung, die der Vergleichsoperator des Dialekts auf
Strings anwendet — ein Programm kann die Satzreihenfolge damit
vorhersagen. Praktisch heißt das: alle Großbuchstaben `A``Z` stehen vor
allen Kleinbuchstaben `a``z`, und Zeichen jenseits von ASCII (`Ä`, `ß`,
`€`) stehen nach beiden, in der Reihenfolge ihrer Codepoints. Beispiel:
`Apfel` < `Birne` < `apfel` < `birne` < `Äpfel`.
Eine sprachabhängige (locale-abhängige) Sortierung gibt es bewusst nicht:
sie machte die Reihenfolge plattformabhängig. Das Vorbild ordnete nach den
Zeichencodes einer festen Codepage; Codepoints sind die direkte
Entsprechung unter der Unicode-Entscheidung dieses Projekts.
**Absteigende Indexspalten** (Erweiterung). Die Original-Hilfe kennt bei
`CREATEINDEX` keine Sortierrichtung. Terminal Basic ordnet eine Spalte
absteigend, wenn ihrem Namen ein `-` vorangestellt ist:
```basic
CREATEINDEX #1, "NachPreis", 0, "Nachname", "-Preis"
```
Das ist kollisionsfrei, weil ein Feldname einer `TYPE`-Anweisung nie mit
`-` beginnen kann; Programme des Vorbilds bleiben unverändert gültig.
**`SETMEM` und Fehler 89** (Abweichung). Im Vorbild veränderte
`SETMEM(n&)` den DOS-Fern-Heap um `n` Bytes. Ein DOS-Speichermodell gibt
es hier nicht; stattdessen ist `SETMEM` die Obergrenze des ISAM-Puffers:
sie wird um `n` Bytes verändert (negativ verkleinert, positiv vergrößert)
und die danach verfügbare Größe geliefert. Der Vorgabewert ist **65536
Bytes**. Gegen die Grenze zählen die Satzbytes einer laufenden Operation
und das Rücknahmeprotokoll einer offenen Transaktion. Reicht der Puffer
für eine Operation nicht aus, tritt Laufzeitfehler 89 („ISAM -
Insufficient ISAM buffers") auf — nicht ein generischer Fehler und kein
stiller Abbruch.
## 11. Eingebaute Funktionen (Katalog) ## 11. Eingebaute Funktionen (Katalog)
**Strings:** `LEN`, `LEFT$`, `RIGHT$`, `MID$` (Funktion **und** Anweisung), **Strings:** `LEN`, `LEFT$`, `RIGHT$`, `MID$` (Funktion **und** Anweisung),

126
tests/compat/isamcursor.bas Normal file
View File

@@ -0,0 +1,126 @@
' tb-screen: 80x25
' tb-tempdir
' ISAM (Spec isam-datenbank, Anforderungen "Cursorbewegung in der Ordnung
' des aktiven Index" und "Schluesselsuche"): MOVE-Familie, Einfuegereihen-
' folge ohne aktiven Index, EOF/BOF an den Raendern, SEEK-Familie mit
' Praefixsuche, erfolglose Suche und Suche ohne aktiven Index.
' Laeuft in einem temporaeren Verzeichnis.
TYPE Satz
Schluessel AS INTEGER
Nachname AS STRING * 6
Vorname AS STRING * 6
END TYPE
DIM s AS Satz
ON ERROR GOTO Handler
OPEN "cursor.isam" FOR ISAM Satz "Werte" AS #1
' Einfuegereihenfolge 30, 10, 20 — damit sie sich von der Indexordnung
' unterscheidet.
s.Schluessel = 30
s.Nachname = "Cesar"
s.Vorname = "Carl"
INSERT #1, s
s.Schluessel = 10
s.Nachname = "Adam"
s.Vorname = "Anna"
INSERT #1, s
s.Schluessel = 20
s.Nachname = "Adam"
s.Vorname = "Bert"
INSERT #1, s
CREATEINDEX #1, "NachSchluessel", 1, "Schluessel"
CREATEINDEX #1, "NachName", 0, "Nachname", "Vorname"
' --- 6.3: Bewegung ohne aktiven Index folgt der Einfuegereihenfolge ---
SETINDEX #1
PRINT "ohne Index:";
MOVEFIRST #1
DO WHILE NOT EOF(1)
RETRIEVE #1, s
PRINT s.Schluessel;
MOVENEXT #1
LOOP
PRINT
' --- 6.2: Durchlauf in der Ordnung des aktiven Index ---
SETINDEX #1, "NachSchluessel"
PRINT "vorwaerts:";
MOVEFIRST #1
DO WHILE NOT EOF(1)
RETRIEVE #1, s
PRINT s.Schluessel;
MOVENEXT #1
LOOP
PRINT
PRINT "rueckwaerts:";
MOVELAST #1
DO WHILE NOT BOF(1)
RETRIEVE #1, s
PRINT s.Schluessel;
MOVEPREVIOUS #1
LOOP
PRINT
' --- 6.4: MOVENEXT am Ende setzt EOF, Cursor unpositioniert ---
MOVELAST #1
MOVENEXT #1
PRINT "EOF nach MOVENEXT am Ende:"; EOF(1); "BOF:"; BOF(1)
RETRIEVE #1, s
' --- 6.4: MOVEPREVIOUS am Anfang setzt BOF, Cursor unpositioniert ---
MOVEFIRST #1
MOVEPREVIOUS #1
PRINT "BOF nach MOVEPREVIOUS am Anfang:"; BOF(1); "EOF:"; EOF(1)
RETRIEVE #1, s
' --- 6.5: SEEKGT, SEEKGE, SEEKEQ mit den Schluesseln 10, 20, 30 ---
SETINDEX #1, "NachSchluessel"
SEEKGT #1, 20
RETRIEVE #1, s
PRINT "SEEKGT 20 ->"; s.Schluessel
SEEKGE #1, 20
RETRIEVE #1, s
PRINT "SEEKGE 20 ->"; s.Schluessel
SEEKGE #1, 15
RETRIEVE #1, s
PRINT "SEEKGE 15 ->"; s.Schluessel
SEEKEQ #1, 10
RETRIEVE #1, s
PRINT "SEEKEQ 10 ->"; s.Schluessel
' --- 6.5: Praefixsuche ueber einen mehrspaltigen Index ---
SETINDEX #1, "NachName"
SEEKGE #1, "Adam"
RETRIEVE #1, s
PRINT "Praefix Adam ->"; RTRIM$(s.Nachname); "/"; RTRIM$(s.Vorname)
' SEEKEQ mit unvollstaendigem Schluessel schlaegt immer fehl (Original-Hilfe).
SEEKEQ #1, "Adam"
PRINT "SEEKEQ mit Praefix, EOF:"; EOF(1)
' Mit beiden Werten findet SEEKEQ den Satz.
SEEKEQ #1, "Adam", "Bert"
RETRIEVE #1, s
PRINT "SEEKEQ Adam/Bert ->"; s.Schluessel
' --- 6.6: erfolglose Suche setzt EOF, Cursor unpositioniert ---
SETINDEX #1, "NachSchluessel"
SEEKGT #1, 30
PRINT "SEEKGT 30, EOF:"; EOF(1)
RETRIEVE #1, s
' --- 6.7: Suche ueber den NULL-Index meldet 87 ---
SETINDEX #1
SEEKEQ #1, 10
CLOSE #1
PRINT "fertig"
END
Handler:
PRINT "ERR="; ERR
RESUME NEXT

View File

@@ -0,0 +1,18 @@
ohne Index: 30 10 20
vorwaerts: 10 20 30
rueckwaerts: 30 20 10
EOF nach MOVENEXT am Ende:-1 BOF: 0
ERR= 85
BOF nach MOVEPREVIOUS am Anfang:-1 EOF: 0
ERR= 85
SEEKGT 20 -> 30
SEEKGE 20 -> 20
SEEKGE 15 -> 20
SEEKEQ 10 -> 10
Praefix Adam ->Adam/Anna
SEEKEQ mit Praefix, EOF:-1
SEEKEQ Adam/Bert -> 20
SEEKGT 30, EOF:-1
ERR= 85
ERR= 87
fertig

View File

@@ -0,0 +1,100 @@
' tb-screen: 80x25
' tb-tempdir
' ISAM (Spec isam-datenbank, Anforderung "Indexverwaltung"): CREATEINDEX
' mit einer und mit mehreren Spalten, eindeutig und mehrdeutig, auf- und
' absteigend; SETINDEX/GETINDEX$, DELETEINDEX und die Fehler 83 und 84.
' Laeuft in einem temporaeren Verzeichnis.
TYPE Person
Nachname AS STRING * 8
Vorname AS STRING * 8
Alter AS INTEGER
END TYPE
DIM p AS Person
ON ERROR GOTO Handler
OPEN "leute.isam" FOR ISAM Person "Leute" AS #1
p.Nachname = "Meier"
p.Vorname = "Anna"
p.Alter = 30
INSERT #1, p
p.Nachname = "Meier"
p.Vorname = "Bert"
p.Alter = 50
INSERT #1, p
p.Nachname = "Adam"
p.Vorname = "Cara"
p.Alter = 40
INSERT #1, p
' --- 5.2: eine Spalte, mehrdeutig, aufsteigend ---
CREATEINDEX #1, "NachName", 0, "Nachname"
SETINDEX #1, "NachName"
PRINT "einspaltig aufsteigend:";
GOSUB Durchlauf
' --- 5.2: eine Spalte, absteigend ---
CREATEINDEX #1, "AlterAb", 0, "-Alter"
SETINDEX #1, "AlterAb"
PRINT "absteigend nach Alter:";
GOSUB Durchlauf
' --- 5.2: mehrspaltig, eindeutig ---
CREATEINDEX #1, "VollName", 1, "Nachname", "Vorname"
SETINDEX #1, "VollName"
PRINT "mehrspaltig eindeutig:";
GOSUB Durchlauf
' --- 5.2: mehrspaltig gemischte Richtung ---
CREATEINDEX #1, "NachAbAlter", 0, "Nachname", "-Alter"
SETINDEX #1, "NachAbAlter"
PRINT "Nachname auf, Alter ab:";
GOSUB Durchlauf
' --- 5.4: GETINDEX$ liefert den aktiven Index zurueck ---
SETINDEX #1, "NachName"
PRINT "aktiv: ["; GETINDEX$(1); "]"
' Ohne Namen gilt der NULL-Index (Einfuegereihenfolge).
SETINDEX #1
PRINT "NULL-Index: ["; GETINDEX$(1); "]"
' --- 5.4: nach SETINDEX ist der erste Satz der neuen Ordnung aktuell ---
SETINDEX #1, "NachName"
RETRIEVE #1, p
PRINT "erster nach NachName: "; RTRIM$(p.Nachname); "/"; RTRIM$(p.Vorname)
SETINDEX #1, "AlterAb"
RETRIEVE #1, p
PRINT "erster nach AlterAb: "; RTRIM$(p.Nachname); "/"; RTRIM$(p.Vorname)
SETINDEX #1
RETRIEVE #1, p
PRINT "erster im NULL-Index: "; RTRIM$(p.Nachname); "/"; RTRIM$(p.Vorname)
' --- 5.5: unbekannter Index meldet 83 ---
SETINDEX #1, "GibtsNicht"
' --- 5.5: unbekannte Spalte meldet 84 ---
CREATEINDEX #1, "Falsch", 0, "GibtsNicht"
' --- 5.3: DELETEINDEX; danach ist der Index nicht mehr setzbar ---
DELETEINDEX #1, "NachName"
PRINT "Index geloescht"
SETINDEX #1, "NachName"
CLOSE #1
PRINT "fertig"
END
' Sätze in der Ordnung des aktiven Index ausgeben.
Durchlauf:
MOVEFIRST #1
DO WHILE NOT EOF(1)
RETRIEVE #1, p
PRINT " "; RTRIM$(p.Nachname); "/"; RTRIM$(p.Vorname); p.Alter;
MOVENEXT #1
LOOP
PRINT
RETURN
Handler:
PRINT "ERR="; ERR
RESUME NEXT

View File

@@ -0,0 +1,14 @@
einspaltig aufsteigend: Adam/Cara 40 Meier/Anna 30 Meier/Bert 50
absteigend nach Alter: Meier/Bert 50 Adam/Cara 40 Meier/Anna 30
mehrspaltig eindeutig: Adam/Cara 40 Meier/Anna 30 Meier/Bert 50
Nachname auf, Alter ab: Adam/Cara 40 Meier/Bert 50 Meier/Anna 30
aktiv: [NachName]
NULL-Index: []
erster nach NachName: Adam/Cara
erster nach AlterAb: Meier/Bert
erster im NULL-Index: Meier/Anna
ERR= 83
ERR= 84
Index geloescht
ERR= 83
fertig

View File

@@ -0,0 +1,76 @@
' tb-screen: 80x25
' tb-tempdir
' ISAM (Spec isam-datenbank, Anforderungen "Pufferverwaltung mit benanntem
' Fehler" und "Vergleichsreihenfolge fuer Indizes"): SETMEM aendert die
' Puffergrenze und liefert die verfuegbare Groesse, ein Ueberschreiten meldet
' 89; die Indexordnung stimmt mit dem Stringvergleich des Dialekts ueberein.
' Laeuft in einem temporaeren Verzeichnis.
TYPE Wort
Text AS STRING * 6
END TYPE
DIM w AS Wort
ON ERROR GOTO Handler
OPEN "worte.isam" FOR ISAM Wort "Worte" AS #1
CREATEINDEX #1, "NachText", 0, "Text"
SETINDEX #1, "NachText"
' --- 9.4: Gross-/Kleinschreibung und Zeichen jenseits von ASCII ---
' Eingefuegt in willkuerlicher Reihenfolge; der Index ordnet nach
' Unicode-Codepoints, also genau wie der Vergleichsoperator des Dialekts.
w.Text = "birne"
INSERT #1, w
w.Text = "Apfel"
INSERT #1, w
w.Text = "Äpfel"
INSERT #1, w
w.Text = "apfel"
INSERT #1, w
w.Text = "Birne"
INSERT #1, w
PRINT "Indexordnung:";
DIM voriges AS STRING
voriges = ""
passt% = -1
MOVEFIRST #1
DO WHILE NOT EOF(1)
RETRIEVE #1, w
PRINT " "; RTRIM$(w.Text);
' Jedes Paar in Folge muss auch dem Operator < genuegen.
IF voriges <> "" THEN
IF NOT (voriges < w.Text) THEN passt% = 0
END IF
voriges = w.Text
MOVENEXT #1
LOOP
PRINT
PRINT "Indexordnung passt zu <:"; passt%
' --- 9.2: SETMEM liefert die verfuegbare Groesse ---
frei& = SETMEM(0)
PRINT "Puffer vorgabe:"; frei&
frei& = SETMEM(-1024)
PRINT "nach SETMEM(-1024):"; frei&
frei& = SETMEM(1024)
PRINT "nach SETMEM(1024):"; frei&
' --- 9.2: unter die Satzgroesse gesenkt meldet ein INSERT 89 ---
frei& = SETMEM(-65530)
PRINT "knapper Puffer:"; frei&
w.Text = "zuviel"
INSERT #1, w
' Wieder aufgemacht, geht es weiter.
frei& = SETMEM(65530)
w.Text = "zebra"
INSERT #1, w
PRINT "nach Aufstocken Saetze:"; LOF(1)
CLOSE #1
PRINT "fertig"
END
Handler:
PRINT "ERR="; ERR
RESUME NEXT

View File

@@ -0,0 +1,9 @@
Indexordnung: Apfel Birne apfel birne Äpfel
Indexordnung passt zu <:-1
Puffer vorgabe: 65536
nach SETMEM(-1024): 64512
nach SETMEM(1024): 65536
knapper Puffer: 6
ERR= 89
nach Aufstocken Saetze: 6
fertig

121
tests/compat/isamsaetze.bas Normal file
View File

@@ -0,0 +1,121 @@
' tb-screen: 80x25
' tb-tempdir
' ISAM (Spec isam-datenbank, Anforderung "Satzoperationen auf dem aktuellen
' Satz"): INSERT/RETRIEVE feldgleich, UPDATE/DELETE mit unpositioniertem
' Cursor danach, Fehler 85 ohne aktuellen Satz, Fehler 86 bei Verstoss gegen
' einen eindeutigen Index mit unveraendertem Bestand, und Indexnachfuehrung
' nach jeder Satzoperation. Laeuft in einem temporaeren Verzeichnis.
TYPE Artikel
Nr AS LONG
Name AS STRING * 8
Preis AS DOUBLE
Menge AS INTEGER
END TYPE
DIM a AS Artikel, g AS Artikel
ON ERROR GOTO Handler
OPEN "lager.isam" FOR ISAM Artikel "Artikel" AS #1
CREATEINDEX #1, "NachNr", 1, "Nr"
CREATEINDEX #1, "NachName", 0, "Name"
' --- 7.1: einfuegen, per SEEKEQ finden und feldgleich zurueckelesen ---
a.Nr = 100
a.Name = "Schraube"
a.Preis = 1.25
a.Menge = 7
INSERT #1, a
a.Nr = 200
a.Name = "Mutter"
a.Preis = 0.5
a.Menge = 3
INSERT #1, a
SETINDEX #1, "NachNr"
SEEKEQ #1, 100
RETRIEVE #1, g
PRINT "gelesen:"; g.Nr; RTRIM$(g.Name); g.Preis; g.Menge
PRINT "feldgleich:"; (g.Nr = 100) AND (RTRIM$(g.Name) = "Schraube") AND (g.Preis = 1.25) AND (g.Menge = 7)
' --- 7.3: RETRIEVE ohne positionierten Cursor meldet 85 ---
' Hinter dem letzten Satz gibt es keinen aktuellen Satz mehr.
MOVELAST #1
MOVENEXT #1
RETRIEVE #1, g
' --- 7.3: UPDATE ohne positionierten Cursor meldet 85 ---
MOVELAST #1
MOVENEXT #1
UPDATE #1, a
' --- 7.3: DELETE ohne positionierten Cursor meldet 85 ---
MOVELAST #1
MOVENEXT #1
DELETE #1
' --- 7.5: UPDATE der indizierten Spalte fuehrt den Index nach ---
SETINDEX #1, "NachName"
PRINT "vor UPDATE:";
GOSUB Namen
SEEKEQ #1, "Schraube"
RETRIEVE #1, g
g.Name = "Anker"
UPDATE #1, g
PRINT "nach UPDATE:";
GOSUB Namen
' --- 7.4: Verstoss gegen einen eindeutigen Index meldet 86 ---
PRINT "Bestand vor 86:"; LOF(1)
a.Nr = 200
a.Name = "Doppelt"
a.Preis = 9
a.Menge = 1
INSERT #1, a
PRINT "Bestand nach 86:"; LOF(1)
' --- 7.2: UPDATE und DELETE; Cursor danach unpositioniert ---
SETINDEX #1, "NachNr"
SEEKEQ #1, 200
RETRIEVE #1, g
g.Menge = 99
UPDATE #1, g
SEEKEQ #1, 200
RETRIEVE #1, g
PRINT "nach UPDATE Menge:"; g.Menge
' Der Satz mit Nr 200 ist im Index NachNr der letzte; nach seinem Loeschen
' steht der Cursor am Ende der Tabelle und es gibt keinen aktuellen Satz.
SEEKEQ #1, 200
DELETE #1
PRINT "nach DELETE Bestand:"; LOF(1); "EOF:"; EOF(1)
RETRIEVE #1, g
' Wird dagegen ein Satz mit Nachfolger geloescht, wird dieser der aktuelle.
SETINDEX #1, "NachNr"
a.Nr = 300
a.Name = "Duebel"
a.Preis = 2
a.Menge = 5
INSERT #1, a
SEEKEQ #1, 100
DELETE #1
RETRIEVE #1, g
PRINT "nach DELETE ist aktuell:"; g.Nr
CLOSE #1
PRINT "fertig"
END
' Namen in der Ordnung des Index NachName ausgeben.
Namen:
MOVEFIRST #1
DO WHILE NOT EOF(1)
RETRIEVE #1, g
PRINT " "; RTRIM$(g.Name);
MOVENEXT #1
LOOP
PRINT
RETURN
Handler:
PRINT "ERR="; ERR
RESUME NEXT

View File

@@ -0,0 +1,15 @@
gelesen: 100 Schraube 1.25 7
feldgleich:-1
ERR= 85
ERR= 85
ERR= 85
vor UPDATE: Mutter Schraube
nach UPDATE: Anker Mutter
Bestand vor 86: 2
ERR= 86
Bestand nach 86: 2
nach UPDATE Menge: 99
nach DELETE Bestand: 1 EOF:-1
ERR= 85
nach DELETE ist aktuell: 300
fertig

View File

@@ -0,0 +1,66 @@
' tb-screen: 80x25
' tb-tempdir
' ISAM (Spec isam-datenbank, Anforderung "Datenbank- und Tabellenbindung an
' eine Dateinummer"): Tabelle entsteht beim ersten Oeffnen, Layoutabgleich
' meldet 88, ungueltige Namen melden 81, CLOSE schreibt fest, DELETETABLE
' entfernt die Tabelle und meldet 82, wenn es sie nicht gibt.
' Laeuft in einem temporaeren Verzeichnis.
TYPE Kunde
Nr AS LONG
Name AS STRING * 8
END TYPE
TYPE Anders
Nr AS INTEGER
END TYPE
DIM k AS Kunde, a AS Anders
ON ERROR GOTO Handler
' --- 4.1/4.2: Tabelle wird beim ersten Oeffnen angelegt ---
OPEN "kunden.isam" FOR ISAM Kunde "Kunden" AS #1
PRINT "angelegt, Saetze:"; LOF(1)
k.Nr = 1
k.Name = "Anton"
INSERT #1, k
k.Nr = 2
k.Name = "Berta"
INSERT #1, k
PRINT "nach INSERT:"; LOF(1)
' --- 4.5: CLOSE loest die Bindung und schreibt fest ---
CLOSE #1
OPEN "kunden.isam" FOR ISAM Kunde "Kunden" AS #1
PRINT "nach erneutem Oeffnen:"; LOF(1)
CLOSE #1
' --- 4.3: abweichendes Satzlayout meldet 88 ---
OPEN "kunden.isam" FOR ISAM Anders "Kunden" AS #2
' --- 4.4: ungueltiger Tabellenname meldet 81 ---
OPEN "kunden.isam" FOR ISAM Kunde "1Ungueltig" AS #3
' --- 4.4: ungueltiger Datenbankname meldet 81 ---
OPEN "" FOR ISAM Kunde "Kunden" AS #4
' --- 4.6: DELETETABLE fuer eine fehlende Tabelle meldet 82 ---
DELETETABLE "kunden.isam", "GibtsNicht"
' --- 4.6: DELETETABLE entfernt die Tabelle samt Index ---
OPEN "kunden.isam" FOR ISAM Kunde "Kunden" AS #1
CREATEINDEX #1, "NachName", 0, "Name"
CLOSE #1
DELETETABLE "kunden.isam", "Kunden"
PRINT "geloescht"
OPEN "kunden.isam" FOR ISAM Kunde "Kunden" AS #1
PRINT "neu angelegt, Saetze:"; LOF(1)
' Der Index ist mit der Tabelle verschwunden.
SETINDEX #1, "NachName"
Ende:
CLOSE
PRINT "fertig"
END
Handler:
PRINT "ERR="; ERR
RESUME NEXT

View File

@@ -0,0 +1,11 @@
angelegt, Saetze: 0
nach INSERT: 2
nach erneutem Oeffnen: 2
ERR= 88
ERR= 81
ERR= 81
ERR= 82
geloescht
neu angelegt, Saetze: 0
ERR= 83
fertig

View File

@@ -0,0 +1,88 @@
' tb-screen: 80x25
' tb-tempdir
' ISAM (Spec isam-datenbank, Anforderung "Transaktionen mit Ruecknahme"):
' BEGINTRANS/COMMITTRANS schreibt fest, ROLLBACK ALL stellt Saetze und
' Indizes wieder her und beendet die Transaktion, SAVEPOINT liefert eine
' Kennung und ROLLBACK kennung setzt nur bis dorthin zurueck.
' Laeuft in einem temporaeren Verzeichnis.
TYPE Posten
Nr AS INTEGER
Text AS STRING * 6
END TYPE
DIM p AS Posten
ON ERROR GOTO Handler
OPEN "buch.isam" FOR ISAM Posten "Posten" AS #1
CREATEINDEX #1, "NachNr", 1, "Nr"
SETINDEX #1, "NachNr"
' --- 8.1: COMMITTRANS schreibt fest; nach erneutem Oeffnen ist alles da ---
BEGINTRANS
p.Nr = 1
p.Text = "eins"
INSERT #1, p
p.Nr = 2
p.Text = "zwei"
INSERT #1, p
COMMITTRANS
CLOSE #1
OPEN "buch.isam" FOR ISAM Posten "Posten" AS #1
SETINDEX #1, "NachNr"
PRINT "nach COMMITTRANS:";
GOSUB Durchlauf
' --- 8.3: ROLLBACK ALL nimmt Einfuegen und Loeschen zurueck ---
BEGINTRANS
p.Nr = 3
p.Text = "drei"
INSERT #1, p
SEEKEQ #1, 1
DELETE #1
PRINT "in der Transaktion:";
GOSUB Durchlauf
ROLLBACK ALL
PRINT "nach ROLLBACK ALL:";
GOSUB Durchlauf
' Die Transaktion ist beendet — ein weiteres ROLLBACK ist unzulaessig.
ROLLBACK ALL
' --- 8.4: SAVEPOINT und ROLLBACK kennung ---
BEGINTRANS
p.Nr = 4
p.Text = "vier"
INSERT #1, p
s% = SAVEPOINT
PRINT "Sicherungspunkt:"; s%
p.Nr = 5
p.Text = "fuenf"
INSERT #1, p
p.Nr = 6
p.Text = "sechs"
INSERT #1, p
PRINT "vor ROLLBACK:";
GOSUB Durchlauf
ROLLBACK s%
PRINT "nach ROLLBACK s:";
GOSUB Durchlauf
' Die Transaktion laeuft weiter und laesst sich festschreiben.
COMMITTRANS
PRINT "nach COMMITTRANS:";
GOSUB Durchlauf
CLOSE #1
PRINT "fertig"
END
Durchlauf:
MOVEFIRST #1
DO WHILE NOT EOF(1)
RETRIEVE #1, p
PRINT " "; RTRIM$(p.Text);
MOVENEXT #1
LOOP
PRINT
RETURN
Handler:
PRINT "ERR="; ERR
RESUME NEXT

View File

@@ -0,0 +1,9 @@
nach COMMITTRANS: eins zwei
in der Transaktion: zwei drei
nach ROLLBACK ALL: eins zwei
ERR= 5
Sicherungspunkt: 1
vor ROLLBACK: eins zwei vier fuenf sechs
nach ROLLBACK s: eins zwei vier
nach COMMITTRANS: eins zwei vier
fertig