Skip to main content

ironwork_rt/sql/
run.rs

1//! Running an EXEC SQL statement: what it asks of the executor (`SqlHost`), its call to the
2//! database, the cursor and unit-of-work state the session keeps, the SQLCA, and the SQLCODE and
3//! warning WHENEVER tests.
4
5use super::host::{self, COLUMN_COUNT, TRUNCATED, Warnings};
6use super::{Answer, Call, Database, Outcome, Session, Value};
7use crate::abend::Abend;
8use crate::host::Host;
9use crate::lir::{AbendId, HostPlace, SqlEntry, SqlStatement, Sqlca};
10use crate::storage::Loc;
11use crate::vocab::Pos;
12
13type R<T> = Result<T, Abend>;
14
15/// Deadlock or timeout: the backend has rolled the unit of work back.
16const DEADLOCK: i32 = -911;
17
18/// What a statement asks of the executor running it beyond `Host`.
19pub trait SqlHost<'w, P: Copy, S>: Host<P> {
20    /// The run unit's database session, when one is attached.
21    fn session(&mut self) -> Option<&mut Session<'w>>;
22    /// A CICS task, whose unit of work SYNCPOINT ends rather than EXEC SQL.
23    fn in_task(&self) -> bool;
24    fn program_id(&self) -> String;
25    fn text(&self, text: &S) -> String;
26    /// Where a host variable is written, which a program check reading it names.
27    fn place_pos(&self, place: P) -> Pos;
28    /// The abend a host variable with no SQL type gives when its statement reaches it.
29    fn untyped(&mut self, abend: AbendId) -> Abend;
30    /// Tells the observer, for the input trace, the operand of an operation an input could steer.
31    fn sink(&mut self, kind: &'static str, pos: Pos, operand: &str);
32    /// An indicator variable's storage; an executor whose place for an indicator array named
33    /// without subscripts is not its first element locates that element here.
34    fn locate_indicator(&mut self, place: P) -> R<Loc> {
35        self.locate(place, false)
36    }
37}
38
39/// What WHENEVER tests after a statement.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub struct Ran {
42    pub sqlcode: i32,
43    /// SQLWARN0.
44    pub warned: bool,
45}
46
47fn session<'a, 'w: 'a, P: Copy + 'a, S: 'a>(x: &'a mut impl SqlHost<'w, P, S>) -> &'a mut Session<'w> {
48    x.session().expect("a database is attached")
49}
50
51/// Runs a statement and fills the SQLCA; None for a declaration, which does nothing.
52pub fn run<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, entry: &SqlEntry<P, S>, sqlca: &Sqlca<P>, pos: Pos) -> R<Option<Ran>> {
53    let verb = x.text(&entry.verb);
54    let refused = |why: String| Abend { code: "EXEC".into(), message: format!("EXEC SQL {verb} was reached: {why}"), pos, file: None };
55    if let SqlStatement::Connect { what, location } = &entry.statement {
56        // A location the trace cannot read is left to the refusal, so tracing never changes how the run ends.
57        if !location.is_empty()
58            && let Ok(Ok(values)) = host::traced(x, location)
59        {
60            x.sink("connection-target", pos, &values.iter().map(Value::text).collect::<String>());
61        }
62        return Err(refused(format!("ironwork for COBOL does not run {}", x.text(what))));
63    }
64    if x.session().is_none() {
65        return Err(refused("no database is attached to the run".into()));
66    }
67    let (program, text, ordinal) = (x.program_id(), x.text(&entry.text), entry.ordinal);
68    let mut warnings = Warnings::default();
69    let mut outcome = match &entry.statement {
70        SqlStatement::Query { inputs, into } => match host::inputs(x, inputs)? {
71            Err(e) => Outcome::error(e.code, e.state),
72            Ok(values) => {
73                let answer = database(x, &Call { program: &program, ordinal, verb: &verb, cursor: None, text: &text, inputs: &values }, |db, c| db.execute(c), pos)?;
74                single_row(x, answer, into, &mut warnings)?
75            }
76        },
77        SqlStatement::Change { delete, inputs, current_of } => {
78            let current_of = current_of.as_ref().map(|c| x.text(c));
79            let position = current_of.as_deref().map(|c| session(x).cursor(&program, c).map(|c| c.positioned));
80            match (position, host::inputs(x, inputs)?) {
81                (Some(None), _) => Outcome::error(-507, "24501"),
82                (Some(Some(false)), _) => Outcome::error(-508, "24504"),
83                (_, Err(e)) => Outcome::error(e.code, e.state),
84                (_, Ok(values)) => {
85                    let mut answer = database(x, &Call { program: &program, ordinal, verb: &verb, cursor: current_of.as_deref(), text: &text, inputs: &values }, |db, c| db.execute(c), pos)?;
86                    // Db2 answers a searched change that finds no row with +100.
87                    if current_of.is_none() && answer.sqlcode == 0 && answer.affected == 0 {
88                        answer = Outcome::error(100, "02000");
89                    }
90                    if let (Some(c), true, true) = (&current_of, *delete, answer.sqlcode >= 0)
91                        && let Some(open) = session(x).cursor(&program, c)
92                    {
93                        open.positioned = false;
94                    }
95                    answer
96                }
97            }
98        }
99        SqlStatement::Open { cursor, inputs } => {
100            let cursor = x.text(cursor);
101            if session(x).cursor(&program, &cursor).is_some() {
102                Outcome::error(-502, "24502")
103            } else {
104                match host::inputs(x, inputs)? {
105                    Err(e) => Outcome::error(e.code, e.state),
106                    Ok(values) => {
107                        let answer = database(x, &Call { program: &program, ordinal, verb: "OPEN", cursor: Some(&cursor), text: &text, inputs: &values }, |db, c| db.open(c), pos)?;
108                        if answer.sqlcode >= 0 {
109                            session(x).opened(&program, &cursor, entry.with_hold);
110                        }
111                        answer
112                    }
113                }
114            }
115        }
116        SqlStatement::Fetch { cursor, into } => {
117            let cursor = x.text(cursor);
118            if session(x).cursor(&program, &cursor).is_none() {
119                Outcome::error(-501, "24501")
120            } else {
121                let answer = database(x, &Call { program: &program, ordinal, verb: "FETCH", cursor: Some(&cursor), text: &text, inputs: &[] }, |db, c| db.fetch(c), pos)?;
122                if answer.rows.len() > 1 {
123                    return Err(Abend { code: "SQL".into(), message: format!("the database answered FETCH {cursor} with {} rows", answer.rows.len()), pos, file: None });
124                }
125                let on_row = answer.sqlcode >= 0 && answer.rows.len() == 1;
126                if let Some(open) = session(x).cursor(&program, &cursor) {
127                    open.positioned = on_row;
128                }
129                let fetched = single_row(x, answer, into, &mut warnings)?;
130                Outcome { affected: i64::from(on_row), ..fetched }
131            }
132        }
133        SqlStatement::Close { cursor } => {
134            let cursor = x.text(cursor);
135            if session(x).cursor(&program, &cursor).is_none() {
136                Outcome::error(-501, "24501")
137            } else {
138                let answer = database(x, &Call { program: &program, ordinal, verb: "CLOSE", cursor: Some(&cursor), text: &text, inputs: &[] }, |db, c| db.close(c), pos)?;
139                if answer.sqlcode >= 0 {
140                    session(x).closed(&program, &cursor);
141                }
142                answer
143            }
144        }
145        SqlStatement::Commit if x.in_task() => Outcome::error(-925, "2D521"),
146        SqlStatement::Rollback if x.in_task() => Outcome::error(-926, "2D521"),
147        SqlStatement::Commit => {
148            let answer = database(x, &Call { program: &program, ordinal, verb: "COMMIT", cursor: None, text: &text, inputs: &[] }, |db, c| db.commit(c), pos)?;
149            if answer.sqlcode >= 0 {
150                session(x).committed();
151            }
152            answer
153        }
154        SqlStatement::Rollback => {
155            let answer = database(x, &Call { program: &program, ordinal, verb: "ROLLBACK", cursor: None, text: &text, inputs: &[] }, |db, c| db.rollback(c), pos)?;
156            if answer.sqlcode >= 0 {
157                session(x).rolled_back();
158            }
159            answer
160        }
161        SqlStatement::Declaration => return Ok(None),
162        SqlStatement::Unsupported(what) => return Err(refused(format!("ironwork for COBOL does not run {}", x.text(what)))),
163        SqlStatement::Connect { .. } => unreachable!("CONNECT is refused before the session is asked"),
164    };
165    if outcome.sqlcode == DEADLOCK {
166        session(x).rolled_back();
167    }
168    warnings[0] = warnings[1..].iter().any(|&w| w);
169    if outcome.sqlcode == 0 && outcome.sqlstate == "00000" {
170        if warnings[TRUNCATED] {
171            outcome.sqlstate = "01004".into();
172        } else if warnings[COLUMN_COUNT] {
173            outcome.sqlstate = "01503".into();
174        }
175    }
176    host::sqlca(x, sqlca, &outcome, &warnings);
177    Ok(Some(Ran { sqlcode: outcome.sqlcode, warned: warnings[0] }))
178}
179
180/// One call to the database. Any statement but COMMIT and ROLLBACK leaves work to settle.
181fn database<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, call: &Call, run: impl FnOnce(&mut dyn Database, &Call) -> Answer, pos: Pos) -> R<Outcome> {
182    let session = session(x);
183    session.pending |= !matches!(call.verb, "COMMIT" | "ROLLBACK");
184    run(&mut *session.database, call).map_err(|a| Abend { code: a.code.into(), message: a.message, pos, file: None })
185}
186
187/// A SELECT INTO's or a FETCH's answer: one row is assigned, none is +100, more than one is -811.
188fn single_row<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, answer: Outcome, into: &[HostPlace<P>], warnings: &mut Warnings) -> R<Outcome> {
189    if answer.sqlcode < 0 {
190        return Ok(answer);
191    }
192    Ok(match answer.rows.len() {
193        0 => Outcome::error(100, "02000"),
194        1 => match host::assign(x, into, &answer.rows[0], warnings)? {
195            Ok(()) => answer,
196            Err(e) => Outcome::error(e.code, e.state),
197        },
198        _ => Outcome::error(-811, "21000"),
199    })
200}