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

View File

@@ -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"
)
}

View File

@@ -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)]

View File

@@ -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) {

View File

@@ -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)
);
}
}