Skip to main content

ironwork_rt/sql/
database.rs

1//! What answers SQL statements. A backend sees each statement's identity, canonical text and input
2//! values, and answers with rows or a code; the runtime decides what those rows mean for the
3//! program, so every backend answers alike.
4
5use super::Value;
6use std::collections::HashMap;
7
8/// One statement as a backend receives it.
9#[derive(Clone, Copy, Debug)]
10pub struct Call<'a> {
11    pub program: &'a str,
12    /// The statement's place among its program's EXEC SQL blocks, from 1.
13    pub ordinal: u32,
14    /// SELECT, INSERT, UPDATE, DELETE, OPEN, FETCH, CLOSE, COMMIT or ROLLBACK.
15    pub verb: &'a str,
16    pub cursor: Option<&'a str>,
17    /// The canonical text, with `?` for each input.
18    pub text: &'a str,
19    pub inputs: &'a [Value],
20}
21
22/// A backend's answer.
23#[derive(Clone, Debug, PartialEq)]
24pub struct Outcome {
25    pub sqlcode: i32,
26    pub sqlstate: String,
27    /// Rows an INSERT, UPDATE or DELETE affected: SQLERRD(3).
28    pub affected: i64,
29    pub rows: Vec<Vec<Value>>,
30    /// SQLERRMC's message tokens.
31    pub tokens: String,
32}
33
34impl Outcome {
35    pub fn ok() -> Self {
36        Self { sqlcode: 0, sqlstate: "00000".into(), affected: 0, rows: Vec::new(), tokens: String::new() }
37    }
38
39    pub fn rows(rows: Vec<Vec<Value>>) -> Self {
40        Self { rows, ..Self::ok() }
41    }
42
43    pub fn error(sqlcode: i32, sqlstate: &str) -> Self {
44        Self { sqlcode, sqlstate: sqlstate.into(), ..Self::ok() }
45    }
46}
47
48/// Why a backend could not answer at all, which ends the run: a replay that does not hold the
49/// call (`SQLR`), or a connection that failed (`SQL`).
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct Abandoned {
52    pub code: &'static str,
53    pub message: String,
54}
55
56pub type Answer = Result<Outcome, Abandoned>;
57
58pub trait Database {
59    /// SELECT INTO, SET, VALUES INTO, INSERT, UPDATE and DELETE.
60    fn execute(&mut self, call: &Call) -> Answer;
61    fn open(&mut self, call: &Call) -> Answer;
62    /// One row, or no row at the end.
63    fn fetch(&mut self, call: &Call) -> Answer;
64    fn close(&mut self, call: &Call) -> Answer;
65    fn commit(&mut self, call: &Call) -> Answer;
66    fn rollback(&mut self, call: &Call) -> Answer;
67    /// Closes every cursor, held ones too, as the end of a CICS task does, so the next task on the
68    /// same connection finds none open. A backend without cursors of its own has nothing to close.
69    fn close_all(&mut self) -> Result<(), Abandoned> {
70        Ok(())
71    }
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct OpenCursor {
76    pub with_hold: bool,
77    /// On a row, so a positioned UPDATE or DELETE has one to change.
78    pub positioned: bool,
79}
80
81/// A run unit's connection to its database, and the state the runtime keeps rather than asking
82/// the backend, so every backend answers alike.
83pub struct Session<'w> {
84    pub database: &'w mut dyn Database,
85    /// Open cursors by program and cursor name, as two programs may declare the same name.
86    cursors: HashMap<(String, String), OpenCursor>,
87    /// Whether any statement has reached the database since the last COMMIT or ROLLBACK.
88    pub pending: bool,
89}
90
91impl<'w> Session<'w> {
92    pub fn new(database: &'w mut (dyn Database + '_)) -> Self {
93        Self { database, cursors: HashMap::new(), pending: false }
94    }
95
96    pub fn cursor(&mut self, program: &str, name: &str) -> Option<&mut OpenCursor> {
97        self.cursors.get_mut(&(program.to_owned(), name.to_owned()))
98    }
99
100    pub fn opened(&mut self, program: &str, name: &str, with_hold: bool) {
101        self.cursors.insert((program.to_owned(), name.to_owned()), OpenCursor { with_hold, positioned: false });
102    }
103
104    pub fn closed(&mut self, program: &str, name: &str) {
105        self.cursors.remove(&(program.to_owned(), name.to_owned()));
106    }
107
108    /// COMMIT closes every cursor not declared WITH HOLD, and leaves a held one before its next row.
109    pub fn committed(&mut self) {
110        self.cursors.retain(|_, c| c.with_hold);
111        self.cursors.values_mut().for_each(|c| c.positioned = false);
112        self.pending = false;
113    }
114
115    pub fn rolled_back(&mut self) {
116        self.cursors.clear();
117        self.pending = false;
118    }
119
120    /// Ends a unit of work that no EXEC SQL statement ends: at SYNCPOINT, or at the end of a run
121    /// unit or task. The database is asked only while it holds work or an open cursor, and the
122    /// call names `program` with ordinal 0.
123    pub fn settle(&mut self, program: &str, commit: bool) -> Answer {
124        let answer = if !self.pending && self.cursors.is_empty() {
125            Outcome::ok()
126        } else {
127            let verb = if commit { "COMMIT" } else { "ROLLBACK" };
128            let call = Call { program, ordinal: 0, verb, cursor: None, text: verb, inputs: &[] };
129            if commit { self.database.commit(&call)? } else { self.database.rollback(&call)? }
130        };
131        if commit && answer.sqlcode >= 0 {
132            self.committed();
133        } else {
134            self.rolled_back();
135        }
136        Ok(answer)
137    }
138
139    /// Ends a CICS task: settles its unit of work, then closes the held cursors a commit leaves
140    /// open, as the end of a task closes every cursor.
141    pub fn end_task(&mut self, program: &str, commit: bool) -> Answer {
142        let answer = self.settle(program, commit)?;
143        if !self.cursors.is_empty() {
144            self.database.close_all()?;
145            self.cursors.clear();
146        }
147        Ok(answer)
148    }
149}