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::dynamic::{self, Kind};
6use super::host::{self, COLUMN_COUNT, TRUNCATED, Warnings};
7use super::{Answer, Call, Database, Outcome, Prepared, Session, SqlError, Value};
8use crate::abend::Abend;
9use crate::host::Host;
10use crate::lir::{AbendId, HostPlace, SqlEntry, SqlStatement, Sqlca};
11use crate::storage::Loc;
12use crate::vocab::Pos;
13
14type R<T> = Result<T, Abend>;
15
16/// Deadlock or timeout: the backend has rolled the unit of work back.
17const DEADLOCK: i32 = -911;
18
19/// Db2 13 for z/OS SQL, PREPARE and EXECUTE IMMEDIATE: "In ... COBOL ... a host variable must be a
20/// varying-length string variable."
21const STRING_RULE: &str = "its statement string is not one varying-length character or graphic string, as Db2 for z/OS requires of COBOL";
22
23/// Where a statement's calls come from.
24#[derive(Clone, Copy)]
25struct At<'a> {
26    program: &'a str,
27    ordinal: u32,
28    pos: Pos,
29}
30
31impl<'a> At<'a> {
32    fn call(self, verb: &'a str, cursor: Option<&'a str>, text: &'a str, inputs: &'a [Value]) -> Call<'a> {
33        Call { program: self.program, ordinal: self.ordinal, verb, cursor, text, inputs }
34    }
35}
36
37/// What a statement asks of the executor running it beyond `Host`.
38pub trait SqlHost<'w, P: Copy, S>: Host<P> {
39    /// The run unit's database session, when one is attached.
40    fn session(&mut self) -> Option<&mut Session<'w>>;
41    /// A CICS task, whose unit of work SYNCPOINT ends rather than EXEC SQL.
42    fn in_task(&self) -> bool;
43    fn program_id(&self) -> String;
44    fn text(&self, text: &S) -> String;
45    /// Where a host variable is written, which a program check reading it names.
46    fn place_pos(&self, place: P) -> Pos;
47    /// The abend a host variable with no SQL type gives when its statement reaches it.
48    fn untyped(&mut self, abend: AbendId) -> Abend;
49    /// Tells the observer, for the input trace, the operand of an operation an input could steer.
50    fn sink(&mut self, kind: &'static str, pos: Pos, operand: &str);
51    /// An indicator variable's storage; an executor whose place for an indicator array named
52    /// without subscripts is not its first element locates that element here.
53    fn locate_indicator(&mut self, place: P) -> R<Loc> {
54        self.locate(place, false)
55    }
56}
57
58/// What WHENEVER tests after a statement.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub struct Ran {
61    pub sqlcode: i32,
62    /// SQLWARN0.
63    pub warned: bool,
64}
65
66fn session<'a, 'w: 'a, P: Copy + 'a, S: 'a>(x: &'a mut impl SqlHost<'w, P, S>) -> &'a mut Session<'w> {
67    x.session().expect("a database is attached")
68}
69
70/// Runs a statement and fills the SQLCA; None for a declaration, which does nothing.
71pub 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>> {
72    let verb = x.text(&entry.verb);
73    let refused = |why: String| Abend { code: "EXEC".into(), message: format!("EXEC SQL {verb} was reached: {why}"), pos, file: None };
74    if let SqlStatement::Connect { what, location } = &entry.statement {
75        // A location the trace cannot read is left to the refusal, so tracing never changes how the run ends.
76        if !location.is_empty()
77            && let Ok(Ok(values)) = host::traced(x, location)
78        {
79            x.sink("connection-target", pos, &values.iter().map(Value::text).collect::<String>());
80        }
81        return Err(refused(format!("ironwork for COBOL does not run {}", x.text(what))));
82    }
83    // A statement string goes to the input trace before the run is refused for want of a database,
84    // as CONNECT's location does.
85    let string = match &entry.statement {
86        SqlStatement::Prepare { source, .. } | SqlStatement::ExecuteImmediate { source } => match statement_string(x, source, pos)? {
87            None => return Err(refused(STRING_RULE.into())),
88            string => string,
89        },
90        _ => None,
91    };
92    if x.session().is_none() {
93        return Err(refused("no database is attached to the run".into()));
94    }
95    let (program, text, ordinal) = (x.program_id(), x.text(&entry.text), entry.ordinal);
96    let at = At { program: &program, ordinal, pos };
97    let mut warnings = Warnings::default();
98    let mut outcome = match &entry.statement {
99        SqlStatement::Query { inputs, into } => match host::inputs(x, inputs)? {
100            Err(e) => Outcome::error(e.code, e.state),
101            Ok(values) => {
102                let answer = database(x, &Call { program: &program, ordinal, verb: &verb, cursor: None, text: &text, inputs: &values }, |db, c| db.execute(c), pos)?;
103                single_row(x, answer, into, &mut warnings)?
104            }
105        },
106        SqlStatement::Change { delete, inputs, current_of } => {
107            let current_of = current_of.as_ref().map(|c| x.text(c));
108            let values = host::inputs(x, inputs)?;
109            change(x, at, (&verb, &text), values, *delete, current_of.as_deref())?
110        }
111        SqlStatement::Open { cursor, inputs } => {
112            let cursor = x.text(cursor);
113            if session(x).cursor(&program, &cursor).is_some() {
114                Outcome::error(-502, "24502")
115            } else {
116                match host::inputs(x, inputs)? {
117                    Err(e) => Outcome::error(e.code, e.state),
118                    Ok(values) => {
119                        let answer = database(x, &Call { program: &program, ordinal, verb: "OPEN", cursor: Some(&cursor), text: &text, inputs: &values }, |db, c| db.open(c), pos)?;
120                        if answer.sqlcode >= 0 {
121                            session(x).opened(&program, &cursor, entry.with_hold, None);
122                        }
123                        answer
124                    }
125                }
126            }
127        }
128        SqlStatement::Fetch { cursor, into } => {
129            let cursor = x.text(cursor);
130            if session(x).cursor(&program, &cursor).is_none() {
131                Outcome::error(-501, "24501")
132            } else {
133                let answer = database(x, &Call { program: &program, ordinal, verb: "FETCH", cursor: Some(&cursor), text: &text, inputs: &[] }, |db, c| db.fetch(c), pos)?;
134                if answer.rows.len() > 1 {
135                    return Err(Abend { code: "SQL".into(), message: format!("the database answered FETCH {cursor} with {} rows", answer.rows.len()), pos, file: None });
136                }
137                let on_row = answer.sqlcode >= 0 && answer.rows.len() == 1;
138                if let Some(open) = session(x).cursor(&program, &cursor) {
139                    open.positioned = on_row;
140                }
141                let fetched = single_row(x, answer, into, &mut warnings)?;
142                Outcome { affected: i64::from(on_row), ..fetched }
143            }
144        }
145        SqlStatement::Close { cursor } => {
146            let cursor = x.text(cursor);
147            if session(x).cursor(&program, &cursor).is_none() {
148                Outcome::error(-501, "24501")
149            } else {
150                let answer = database(x, &Call { program: &program, ordinal, verb: "CLOSE", cursor: Some(&cursor), text: &text, inputs: &[] }, |db, c| db.close(c), pos)?;
151                if answer.sqlcode >= 0 {
152                    session(x).closed(&program, &cursor);
153                }
154                answer
155            }
156        }
157        SqlStatement::Commit => end_unit(x, at, &text, true)?,
158        SqlStatement::Rollback => end_unit(x, at, &text, false)?,
159        SqlStatement::Prepare { name, .. } => {
160            let name = x.text(name);
161            match string {
162                None => unreachable!("a PREPARE has its statement string"),
163                Some(Err(e)) => Outcome::error(e.code, e.state),
164                Some(Ok(_)) if session(x).running(&program, &name) => Outcome::error(-519, "24506"),
165                Some(Ok(text)) => {
166                    session(x).prepare(&program, &name, None);
167                    let kind = dynamic::kind(&text);
168                    if kind == Kind::Unacceptable {
169                        Outcome::error(-84, "42612")
170                    } else {
171                        let answer = database(x, &at.call("PREPARE", Some(&name), &text, &[]), |db, c| db.prepare(c), pos)?;
172                        if answer.sqlcode >= 0 {
173                            let markers = dynamic::markers(&text);
174                            session(x).prepare(&program, &name, Some(Prepared { text, query: kind == Kind::Query, markers }));
175                        }
176                        answer
177                    }
178                }
179            }
180        }
181        SqlStatement::ExecuteImmediate { .. } => match string {
182            None => unreachable!("an EXECUTE IMMEDIATE has its statement string"),
183            Some(Err(e)) => Outcome::error(e.code, e.state),
184            Some(Ok(text)) => dynamic_statement(x, at, &text, Ok(Vec::new()), &refused)?,
185        },
186        SqlStatement::Execute { name, inputs } => {
187            let name = x.text(name);
188            match session(x).prepared(&program, &name).cloned() {
189                None | Some(Prepared { query: true, .. }) => Outcome::error(-518, "07003"),
190                Some(p) if p.markers > 0 && p.markers != inputs.len() => Outcome::error(-313, "07001"),
191                Some(p) => {
192                    let values = if p.markers == 0 { Ok(Vec::new()) } else { host::inputs(x, inputs)? };
193                    dynamic_statement(x, at, &p.text, values, &refused)?
194                }
195            }
196        }
197        SqlStatement::OpenPrepared { cursor, statement, inputs } => {
198            let (cursor, statement) = (x.text(cursor), x.text(statement));
199            if session(x).cursor(&program, &cursor).is_some() {
200                Outcome::error(-502, "24502")
201            } else {
202                match session(x).prepared(&program, &statement).cloned() {
203                    None => Outcome::error(-514, "26501"),
204                    Some(Prepared { query: false, .. }) => Outcome::error(-517, "07005"),
205                    Some(p) if p.markers > 0 && p.markers != inputs.len() => Outcome::error(-313, "07001"),
206                    Some(p) => match if p.markers == 0 { Ok(Vec::new()) } else { host::inputs(x, inputs)? } {
207                        Err(e) => Outcome::error(e.code, e.state),
208                        Ok(values) => {
209                            let hold = if entry.with_hold { " WITH HOLD" } else { "" };
210                            let text = format!("DECLARE {cursor} CURSOR{hold} FOR {}", p.text);
211                            let answer = database(x, &at.call("OPEN", Some(&cursor), &text, &values), |db, c| db.open(c), pos)?;
212                            if answer.sqlcode >= 0 {
213                                session(x).opened(&program, &cursor, entry.with_hold, Some(&statement));
214                            }
215                            answer
216                        }
217                    },
218                }
219            }
220        }
221        SqlStatement::Declaration => return Ok(None),
222        SqlStatement::Unsupported(what) => return Err(refused(format!("ironwork for COBOL does not run {}", x.text(what)))),
223        SqlStatement::Connect { .. } => unreachable!("CONNECT is refused before the session is asked"),
224    };
225    if outcome.sqlcode == DEADLOCK {
226        session(x).rolled_back();
227    }
228    warnings[0] = warnings[1..].iter().any(|&w| w);
229    if outcome.sqlcode == 0 && outcome.sqlstate == "00000" {
230        if warnings[TRUNCATED] {
231            outcome.sqlstate = "01004".into();
232        } else if warnings[COLUMN_COUNT] {
233            outcome.sqlstate = "01503".into();
234        }
235    }
236    host::sqlca(x, sqlca, &outcome, &warnings);
237    Ok(Some(Ran { sqlcode: outcome.sqlcode, warned: warnings[0] }))
238}
239
240/// One call to the database. Any statement but COMMIT and ROLLBACK leaves work to settle.
241fn 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> {
242    let session = session(x);
243    session.pending |= !matches!(call.verb, "COMMIT" | "ROLLBACK");
244    run(&mut *session.database, call).map_err(|a| Abend { code: a.code.into(), message: a.message, pos, file: None })
245}
246
247/// COMMIT or ROLLBACK, static or dynamic; in a CICS task the unit of work is CICS's.
248fn end_unit<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, at: At, text: &str, commit: bool) -> R<Outcome> {
249    if x.in_task() {
250        return Ok(if commit { Outcome::error(-925, "2D521") } else { Outcome::error(-926, "2D521") });
251    }
252    let verb = if commit { "COMMIT" } else { "ROLLBACK" };
253    let answer = database(x, &at.call(verb, None, text, &[]), |db, c| if commit { db.commit(c) } else { db.rollback(c) }, at.pos)?;
254    if answer.sqlcode >= 0 {
255        if commit { session(x).committed() } else { session(x).rolled_back() }
256    }
257    Ok(answer)
258}
259
260/// An INSERT, UPDATE or DELETE, `statement` its verb and text: a positioned one needs its cursor on
261/// a row, and a searched one that changes no row is +100, as Db2 answers.
262fn change<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, at: At, statement: (&str, &str), values: Result<Vec<Value>, SqlError>, delete: bool, current_of: Option<&str>) -> R<Outcome> {
263    let position = current_of.map(|c| session(x).cursor(at.program, c).map(|c| c.positioned));
264    let values = match (position, values) {
265        (Some(None), _) => return Ok(Outcome::error(-507, "24501")),
266        (Some(Some(false)), _) => return Ok(Outcome::error(-508, "24504")),
267        (_, Err(e)) => return Ok(Outcome::error(e.code, e.state)),
268        (_, Ok(values)) => values,
269    };
270    let mut answer = database(x, &at.call(statement.0, current_of, statement.1, &values), |db, c| db.execute(c), at.pos)?;
271    if current_of.is_none() && answer.sqlcode == 0 && answer.affected == 0 {
272        answer = Outcome::error(100, "02000");
273    }
274    if let (Some(c), true, true) = (current_of, delete, answer.sqlcode >= 0)
275        && let Some(open) = session(x).cursor(at.program, c)
276    {
277        open.positioned = false;
278    }
279    Ok(answer)
280}
281
282/// The statement string of a PREPARE or EXECUTE IMMEDIATE, normalised, after the input trace is
283/// told of it; None where it is not one varying-length string.
284fn statement_string<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, source: &[crate::lir::HostPlace<P>], pos: Pos) -> R<Option<Result<String, SqlError>>> {
285    match source {
286        [place] if matches!(place.ty, Ok(super::HostType::VarChar(_) | super::HostType::VarGraphic(_)) | Err(_)) => {}
287        _ => return Ok(None),
288    }
289    Ok(Some(host::traced(x, source)?.map(|values| {
290        let string = values.first().map(Value::text).unwrap_or_default();
291        x.sink("dynamic-sql", pos, &string);
292        dynamic::normalise(&string)
293    })))
294}
295
296/// A dynamic statement that is not run by a cursor, as EXECUTE and EXECUTE IMMEDIATE run it.
297fn dynamic_statement<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, at: At, text: &str, values: Result<Vec<Value>, SqlError>, refused: &dyn Fn(String) -> Abend) -> R<Outcome> {
298    let verb = dynamic::verb(text);
299    Ok(match dynamic::kind(text) {
300        Kind::Query => Outcome::error(-518, "07003"),
301        Kind::Unacceptable => Outcome::error(-84, "42612"),
302        Kind::Refused(what) => return Err(refused(format!("ironwork for COBOL does not run {what}"))),
303        Kind::Commit => end_unit(x, at, text, true)?,
304        Kind::Rollback => end_unit(x, at, text, false)?,
305        Kind::Change { delete, current_of } => change(x, at, (&verb, text), values, delete, current_of.as_deref())?,
306        Kind::Other => match values {
307            Err(e) => Outcome::error(e.code, e.state),
308            Ok(values) => database(x, &at.call(&verb, None, text, &values), |db, c| db.execute(c), at.pos)?,
309        },
310    })
311}
312
313/// A SELECT INTO's or a FETCH's answer: one row is assigned, none is +100, more than one is -811.
314fn single_row<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, answer: Outcome, into: &[HostPlace<P>], warnings: &mut Warnings) -> R<Outcome> {
315    if answer.sqlcode < 0 {
316        return Ok(answer);
317    }
318    Ok(match answer.rows.len() {
319        0 => Outcome::error(100, "02000"),
320        1 => match host::assign(x, into, &answer.rows[0], warnings)? {
321            Ok(()) => answer,
322            Err(e) => Outcome::error(e.code, e.state),
323        },
324        _ => Outcome::error(-811, "21000"),
325    })
326}