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

@@ -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::*;