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:
@@ -355,3 +355,36 @@ pub struct Module {
|
||||
pub body: Vec<Stmt>,
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -330,6 +330,30 @@ pub enum Builtin {
|
||||
Doevents,
|
||||
Sleep,
|
||||
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)]
|
||||
|
||||
@@ -710,6 +710,20 @@ impl<'a> P<'a> {
|
||||
}
|
||||
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),
|
||||
_ => {
|
||||
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> {
|
||||
self.advance(); // PRINT bzw. LPRINT
|
||||
let file = if !printer && self.eat(&TokenKind::Hash) {
|
||||
|
||||
@@ -171,6 +171,7 @@ enum ArgK {
|
||||
N, // numerisch
|
||||
S, // String
|
||||
A, // beliebig
|
||||
R, // Satzvariable eines benutzerdefinierten Typs (ISAM)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -274,6 +275,11 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
|
||||
"STACK" => (0, 0, &[], L),
|
||||
"ERDEV" => (0, 0, &[], I),
|
||||
"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,
|
||||
})
|
||||
}
|
||||
@@ -345,6 +351,27 @@ fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> {
|
||||
"STACK" => (0, 1, &[N]),
|
||||
// `CALL SetFormatCC(49)` — Währungsformat nach Ländercode.
|
||||
"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,
|
||||
})
|
||||
}
|
||||
@@ -377,6 +404,7 @@ pub fn lower(module: &Module) -> (Option<hir::HirModule>, Vec<Diagnostic>) {
|
||||
data_marks_name: HashMap::new(),
|
||||
data_marks_line: HashMap::new(),
|
||||
hir_procs: Vec::new(),
|
||||
isam_typ: HashMap::new(),
|
||||
};
|
||||
let hir = s.run(module);
|
||||
(hir, s.diags)
|
||||
@@ -394,6 +422,10 @@ struct Sema {
|
||||
/// Benutzerdefinierte Typen: Name → Index, Definitionen geordnet.
|
||||
udt_ids: HashMap<String, u16>,
|
||||
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.
|
||||
consts: HashMap<String, (Ty, Option<ConstVal>)>,
|
||||
/// Modulvariablen (Schlüssel wie `var_key`).
|
||||
@@ -1512,12 +1544,40 @@ impl Sema {
|
||||
// Vorgabe-Recordlänge des Vorbilds.
|
||||
None => HExpr::Lng(128),
|
||||
};
|
||||
if let Some((ty_name, _table)) = isam {
|
||||
if !ty_name.is_empty() && !self.udt_ids.contains_key(ty_name) {
|
||||
if let Some((ty_name, table)) = isam {
|
||||
let Some(&udt) = self.udt_ids.get(ty_name) else {
|
||||
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(out, scope, HStmtKind::Unsupported("OPEN FOR ISAM"));
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt {
|
||||
b: Builtin::IsamOpen,
|
||||
args: vec![
|
||||
fe,
|
||||
ne,
|
||||
HExpr::Str(table.clone()),
|
||||
HExpr::Str(spalten),
|
||||
HExpr::Lng(udt as i32),
|
||||
],
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Ohne `FOR`-Klausel gilt RANDOM (Vorbild).
|
||||
@@ -2433,6 +2493,13 @@ impl Sema {
|
||||
let a = self.lower_opt_args(args, &lowered);
|
||||
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.
|
||||
_ => self.push(out, scope, HStmtKind::Unsupported(match name {
|
||||
"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,
|
||||
/// sonst Dateinummer).
|
||||
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$)
|
||||
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();
|
||||
for (i, a) in args.iter().enumerate() {
|
||||
if matches!(a, Expr::Missing) {
|
||||
@@ -2509,7 +2640,7 @@ impl Sema {
|
||||
0 => ArgK::N,
|
||||
_ => ArgK::S,
|
||||
}
|
||||
} else if name == "INSTR" {
|
||||
} else if name == "INSTR" || (createindex && i >= 3) {
|
||||
ArgK::S
|
||||
} else {
|
||||
*spec.get(i).unwrap_or(&ArgK::A)
|
||||
@@ -2518,6 +2649,7 @@ impl Sema {
|
||||
ArgK::N => is_num(&at),
|
||||
ArgK::S => is_str(&at),
|
||||
ArgK::A => true,
|
||||
ArgK::R => matches!(at, Ty::Udt(_) | Ty::Unknown),
|
||||
};
|
||||
if !ok {
|
||||
self.err(a.pos(), "Type mismatch");
|
||||
@@ -3369,6 +3501,20 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
"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),
|
||||
"FRE" => b(Builtin::Fre, vec![], Ty::Lng, self),
|
||||
"STACK" => b(Builtin::StackFn, vec![], Ty::Lng, self),
|
||||
@@ -3833,4 +3979,106 @@ mod tests {
|
||||
// BYVAL (Klammern) konvertiert stattdessen
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,3 +11,4 @@ unicode-width.workspace = true
|
||||
jiff.workspace = true
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
redb = "4"
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct RtState {
|
||||
pub screen: TextScreen,
|
||||
/// Offene Dateien nach Dateinummer.
|
||||
pub dateien: crate::fileio::Dateien,
|
||||
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
|
||||
pub isam: crate::isam::Isam,
|
||||
pub print: PrintState,
|
||||
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form.
|
||||
pub tasten: VecDeque<String>,
|
||||
@@ -60,6 +62,7 @@ impl Default for RtState {
|
||||
RtState {
|
||||
screen: TextScreen::new(),
|
||||
dateien: Default::default(),
|
||||
isam: Default::default(),
|
||||
print: PrintState::default(),
|
||||
tasten: VecDeque::new(),
|
||||
abbruch: false,
|
||||
@@ -359,7 +362,31 @@ pub mod ids {
|
||||
pub const STACK_STMT: u16 = 122;
|
||||
pub const ERDEV: u16 = 123;
|
||||
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.
|
||||
@@ -490,6 +517,30 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
|
||||
bi_stack_stmt,
|
||||
bi_erdev,
|
||||
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);
|
||||
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> {
|
||||
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> {
|
||||
st.isam.alles_schliessen();
|
||||
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> {
|
||||
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 })))
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
// ---- 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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
1570
crates/tb-runtime/src/isam.rs
Normal file
1570
crates/tb-runtime/src/isam.rs
Normal file
File diff suppressed because it is too large
Load Diff
439
crates/tb-runtime/src/isam/tests.rs
Normal file
439
crates/tb-runtime/src/isam/tests.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
@@ -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 errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
|
||||
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 host;
|
||||
pub mod screen;
|
||||
|
||||
@@ -1061,6 +1061,30 @@ fn builtin_id(b: Builtin) -> u16 {
|
||||
Builtin::Doevents => ids::DOEVENTS,
|
||||
Builtin::Sleep => ids::SLEEP,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,9 @@ impl Vm {
|
||||
data_ptr: 0,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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 --------------------------------
|
||||
|
||||
#[test]
|
||||
@@ -446,19 +476,34 @@ fn print_zonen_und_tab() {
|
||||
|
||||
#[test]
|
||||
fn unsupported_feature_fehler_73() {
|
||||
// Dokumentiert, aber noch offen: ISAM kommt mit `phase-3-isam`,
|
||||
// die Ereignissteuerung mit Phase 4 → Laufzeitfehler 73.
|
||||
let (ev, _) = run("TYPE T\n f AS INTEGER\nEND TYPE\nOPEN \"db\" FOR ISAM T \"tab\" AS #1");
|
||||
// Dokumentiert, aber noch offen: `SETUEVENT` kommt mit Phase 4
|
||||
// → Laufzeitfehler 73 mit dem Katalogtext des Vorbilds (VBDOS).
|
||||
let (ev, _) = run("SETUEVENT");
|
||||
match ev {
|
||||
RunEvent::Error { code, message, .. } => {
|
||||
assert_eq!(code, 73);
|
||||
// Katalogtext des Vorbilds (VBDOS) für Code 73:
|
||||
assert_eq!(message, "Feature unavailable");
|
||||
}
|
||||
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) ---------------
|
||||
|
||||
/// Vor blockierender Eingabe wird der Bildschirm angezeigt — sonst stünde
|
||||
|
||||
Reference in New Issue
Block a user