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, RESULT_SETS, TRUNCATED, Target, Warnings};
7use super::sqlda::{self, Invalid, Var};
8use super::{Answer, Call, Database, Outcome, Prepared, Session, SqlError, Value};
9use crate::abend::{Abend, AbendCode};
10use crate::host::Host;
11use crate::store::ProgramFacts;
12use crate::lir::{AbendId, HostArray, HostPlace, RowCount, SqlEntry, SqlNames, SqlStatement, Sqlca};
13use crate::storage::Loc;
14use crate::vocab::Pos;
15
16type R<T> = Result<T, Abend>;
17
18/// Deadlock or timeout: the backend has rolled the unit of work back.
19const DEADLOCK: i32 = -911;
20
21/// Db2 13 for z/OS SQL, PREPARE and EXECUTE IMMEDIATE: "In ... COBOL ... a host variable must be a
22/// varying-length string variable."
23const STRING_RULE: &str = "its statement string is not one varying-length character or graphic string, as Db2 for z/OS requires of COBOL";
24
25/// Db2 13 SQL, FETCH and INSERT: FOR n ROWS's host variable "must be an exact numeric type with a
26/// scale of zero".
27const ROWS_RULE: &str = "FOR n ROWS's host variable is not an exact numeric item with no decimal places";
28
29/// The most rows FOR n ROWS may ask for: "k must be in the range, 0<k<=32767".
30const MOST_ROWS: u32 = 32767;
31
32/// Where a statement's calls come from.
33#[derive(Clone, Copy)]
34struct At<'a> {
35    program: &'a str,
36    ordinal: u32,
37    pos: Pos,
38}
39
40impl<'a> At<'a> {
41    fn call(self, verb: &'a str, cursor: Option<&'a str>, text: &'a str, inputs: &'a [Value]) -> Call<'a> {
42        Call { program: self.program, ordinal: self.ordinal, verb, cursor, text, inputs }
43    }
44}
45
46/// What a statement asks of the executor running it beyond `Host`.
47pub trait SqlHost<'w, P: Copy, S>: Host<P> {
48    /// The run unit's database session, when one is attached.
49    fn session(&mut self) -> Option<&mut Session<'w>>;
50    /// A CICS task, whose unit of work SYNCPOINT ends rather than EXEC SQL.
51    fn in_task(&self) -> bool;
52    fn program_id(&self) -> String;
53    fn text(&self, text: &S) -> String;
54    /// Where a host variable is written, which a program check reading it names.
55    fn place_pos(&self, place: P) -> Pos;
56    /// The abend a host variable with no SQL type gives when its statement reaches it.
57    fn untyped(&mut self, abend: AbendId) -> Abend;
58    /// Tells the observer, for the input trace, the operand of an operation an input could steer.
59    fn sink(&mut self, kind: &'static str, pos: Pos, operand: &str);
60    /// A table named without subscripts, an indicator array or a multiple-row statement's
61    /// host-variable array, at its first element; an executor whose place for one is not that
62    /// element locates it here.
63    fn locate_first(&mut self, place: P) -> R<Loc> {
64        self.locate(place, false)
65    }
66}
67
68/// What WHENEVER tests after a statement.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct Ran {
71    pub sqlcode: i32,
72    /// SQLWARN0.
73    pub warned: bool,
74}
75
76fn session<'a, 'w: 'a, P: Copy + 'a, S: 'a>(x: &'a mut impl SqlHost<'w, P, S>) -> &'a mut Session<'w> {
77    x.session().expect("a database is attached")
78}
79
80/// Runs a statement and fills the SQLCA; None for a declaration, which does nothing.
81pub 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>> {
82    let verb = x.text(&entry.verb);
83    let refused = |why: String| Abend { code: "EXEC".into(), message: format!("EXEC SQL {verb} was reached: {why}"), pos, file: None };
84    let not_run = |what: &str| crate::refusal::IWR0061.ending(AbendCode::Exec, format_args!("EXEC SQL {verb} was reached: ironwork for COBOL does not run {what}"), pos);
85    if let SqlStatement::Connect { what, location } = &entry.statement {
86        // A location the trace cannot read is left to the refusal, so tracing never changes how the run ends.
87        if !location.is_empty()
88            && let Ok(Ok(values)) = host::traced(x, location)
89        {
90            x.sink("connection-target", pos, &values.iter().map(Value::text).collect::<String>());
91        }
92        return Err(not_run(&x.text(what)));
93    }
94    // A statement string goes to the input trace before the run is refused for want of a database,
95    // as CONNECT's location does.
96    let string = match &entry.statement {
97        SqlStatement::Prepare { source, .. } | SqlStatement::PrepareInto { source, .. } | SqlStatement::ExecuteImmediate { source } => match statement_string(x, source, pos)? {
98            None => return Err(refused(STRING_RULE.into())),
99            string => string,
100        },
101        _ => None,
102    };
103    if x.session().is_none() {
104        return Err(refused("no database is attached to the run".into()));
105    }
106    let (program, text, ordinal) = (x.program_id(), x.text(&entry.text), entry.ordinal);
107    let at = At { program: &program, ordinal, pos };
108    let mut warnings = Warnings::default();
109    let mut outcome = match &entry.statement {
110        SqlStatement::Query { inputs, into } => match host::inputs(x, inputs)? {
111            Err(e) => Outcome::error(e.code, e.state),
112            Ok(values) => {
113                let answer = database(x, &Call { program: &program, ordinal, verb: &verb, cursor: None, text: &text, inputs: &values }, |db, c| db.execute(c), pos)?;
114                single_row(x, answer, Receivers::Hosts(into), &mut warnings)?
115            }
116        },
117        SqlStatement::Change { delete, inputs, current_of } => {
118            let current_of = current_of.as_ref().map(|c| x.text(c));
119            let values = host::inputs(x, inputs)?;
120            change(x, at, (&verb, &text), values, *delete, current_of.as_deref())?
121        }
122        SqlStatement::Open { cursor, inputs } => {
123            let cursor = x.text(cursor);
124            if session(x).cursor(&program, &cursor).is_some() {
125                Outcome::error(-502, "24502")
126            } else {
127                match host::inputs(x, inputs)? {
128                    Err(e) => Outcome::error(e.code, e.state),
129                    Ok(values) => {
130                        let answer = database(x, &Call { program: &program, ordinal, verb: "OPEN", cursor: Some(&cursor), text: &text, inputs: &values }, |db, c| db.open(c), pos)?;
131                        if answer.sqlcode >= 0 {
132                            session(x).opened(&program, &cursor, entry.with_hold, None);
133                        }
134                        answer
135                    }
136                }
137            }
138        }
139        SqlStatement::Fetch { cursor, into } => {
140            let cursor = x.text(cursor);
141            fetch(x, at, (&cursor, &text), Receivers::Hosts(into), &mut warnings)?
142        }
143        SqlStatement::FetchDescriptor { cursor, descriptor } => {
144            let cursor = x.text(cursor);
145            let at_sqlda = x.locate(*descriptor, false)?.offset;
146            match sqlda::vars(x.mem(), at_sqlda, false) {
147                Err(invalid) => invalid_sqlda(invalid),
148                Ok(vars) => fetch(x, at, (&cursor, &text), Receivers::Vars(&vars), &mut warnings)?,
149            }
150        }
151        SqlStatement::FetchRowset { cursor, rows, into, enabled } => {
152            let cursor = x.text(cursor);
153            if session(x).cursor(&program, &cursor).is_none() {
154                Outcome::error(-501, "24501")
155            } else if !enabled {
156                Outcome::error(-249, "24523")
157            } else {
158                let implicit = session(x).cursor(&program, &cursor).and_then(|c| c.rowset_size).unwrap_or(1);
159                match row_count(x, rows, implicit, &refused)? {
160                    Some(k) if fits(k, into.iter()) => fetch_rowset(x, at, &cursor, &text, k, into, &mut warnings)?,
161                    _ => Outcome::error(-246, "42873"),
162                }
163            }
164        }
165        SqlStatement::InsertRows { inputs, rows, atomic } => match row_count(x, rows, 1, &refused)? {
166            Some(k) if fits(k, inputs.iter()) => match host::input_rows(x, inputs, k as usize)? {
167                Err(e) => Outcome::error(e.code, e.state),
168                Ok(values) => {
169                    let flat = values.concat();
170                    database(x, &at.call(&verb, None, &text, &flat), |db, c| db.insert_rows(c, &values, *atomic), pos)?
171                }
172            },
173            _ => Outcome::error(-246, "42873"),
174        },
175        SqlStatement::Call { procedure, args } => {
176            let procedure = x.text(procedure);
177            match host::inputs(x, args)? {
178                Err(e) => Outcome::error(e.code, e.state),
179                Ok(values) => {
180                    let answer = database(x, &at.call("CALL", Some(&procedure), &text, &values), |db, c| db.call(c), pos)?;
181                    returned(x, answer, &procedure, args, &mut warnings, pos)?
182                }
183            }
184        }
185        SqlStatement::Close { cursor } => {
186            let cursor = x.text(cursor);
187            if session(x).cursor(&program, &cursor).is_none() {
188                Outcome::error(-501, "24501")
189            } else {
190                let answer = database(x, &Call { program: &program, ordinal, verb: "CLOSE", cursor: Some(&cursor), text: &text, inputs: &[] }, |db, c| db.close(c), pos)?;
191                if answer.sqlcode >= 0 {
192                    session(x).closed(&program, &cursor);
193                }
194                answer
195            }
196        }
197        SqlStatement::Commit => end_unit(x, at, &text, true)?,
198        SqlStatement::Rollback => end_unit(x, at, &text, false)?,
199        SqlStatement::Prepare { name, .. } => {
200            let (name, Some(string)) = (x.text(name), string) else { unreachable!("a PREPARE has its statement string") };
201            prepare(x, at, &name, string)?
202        }
203        SqlStatement::PrepareInto { name, descriptor, names, .. } => {
204            let (name, Some(string)) = (x.text(name), string) else { unreachable!("a PREPARE has its statement string") };
205            let answer = prepare(x, at, &name, string)?;
206            match session(x).prepared(&program, &name).cloned().filter(|_| answer.sqlcode >= 0) {
207                Some(p) => {
208                    let described = describe(x, &p, *descriptor, *names, pos)?;
209                    if described.sqlcode < 0 { described } else { answer }
210                }
211                None => answer,
212            }
213        }
214        SqlStatement::Describe { name, descriptor, names } => {
215            let name = x.text(name);
216            match session(x).prepared(&program, &name).cloned() {
217                None => Outcome::error(-516, "26501"),
218                Some(p) => describe(x, &p, *descriptor, *names, pos)?,
219            }
220        }
221        SqlStatement::ExecuteImmediate { .. } => match string {
222            None => unreachable!("an EXECUTE IMMEDIATE has its statement string"),
223            Some(Err(e)) => Outcome::error(e.code, e.state),
224            Some(Ok(text)) => dynamic_statement(x, at, &text, Ok(Vec::new()), &refused)?,
225        },
226        SqlStatement::Execute { name, inputs } => execute(x, at, &x.text(name), Sources::Hosts(inputs), &refused)?,
227        SqlStatement::ExecuteDescriptor { name, descriptor } => execute(x, at, &x.text(name), Sources::Descriptor(*descriptor), &refused)?,
228        SqlStatement::OpenPrepared { cursor, statement, inputs } => open_prepared(x, at, (&x.text(cursor), &x.text(statement)), entry.with_hold, Sources::Hosts(inputs))?,
229        SqlStatement::OpenDescriptor { cursor, statement, descriptor } => open_prepared(x, at, (&x.text(cursor), &x.text(statement)), entry.with_hold, Sources::Descriptor(*descriptor))?,
230        SqlStatement::Declaration => return Ok(None),
231        SqlStatement::Unsupported(what) => return Err(not_run(&x.text(what))),
232        SqlStatement::Connect { .. } => unreachable!("CONNECT is refused before the session is asked"),
233    };
234    if outcome.sqlcode == DEADLOCK {
235        session(x).rolled_back();
236    }
237    warnings[0] = warnings[1..].iter().any(|&w| w);
238    if outcome.sqlcode == 0 && outcome.sqlstate == "00000" {
239        if warnings[TRUNCATED] {
240            outcome.sqlstate = "01004".into();
241        } else if warnings[COLUMN_COUNT] {
242            outcome.sqlstate = "01503".into();
243        }
244    }
245    host::sqlca(x, sqlca, &outcome, &warnings);
246    Ok(Some(Ran { sqlcode: outcome.sqlcode, warned: warnings[0] }))
247}
248
249/// One call to the database. Any statement but COMMIT and ROLLBACK leaves work to settle.
250fn 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> {
251    let session = session(x);
252    session.pending |= !matches!(call.verb, "COMMIT" | "ROLLBACK");
253    run(&mut *session.database, call).map_err(|a| Abend { code: a.code.into(), message: a.message, pos, file: None })
254}
255
256/// COMMIT or ROLLBACK, static or dynamic; in a CICS task the unit of work is CICS's.
257fn end_unit<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, at: At, text: &str, commit: bool) -> R<Outcome> {
258    if x.in_task() {
259        return Ok(if commit { Outcome::error(-925, "2D521") } else { Outcome::error(-926, "2D521") });
260    }
261    let verb = if commit { "COMMIT" } else { "ROLLBACK" };
262    let answer = database(x, &at.call(verb, None, text, &[]), |db, c| if commit { db.commit(c) } else { db.rollback(c) }, at.pos)?;
263    if answer.sqlcode >= 0 {
264        if commit { session(x).committed() } else { session(x).rolled_back() }
265    }
266    Ok(answer)
267}
268
269/// An INSERT, UPDATE or DELETE, `statement` its verb and text: a positioned one needs its cursor on
270/// a row, and a searched one that changes no row is +100, as Db2 answers.
271fn 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> {
272    // The backend's cursor is on the last row it gave, which is the position only when that is one
273    // row; Db2 would change every row of a rowset (C422).
274    if let Some(c) = current_of
275        && session(x).cursor(at.program, c).is_some_and(|c| c.positioned && !c.on_backend_row())
276    {
277        let message = format!("EXEC SQL {} ... WHERE CURRENT OF {c} was reached: ironwork for COBOL does not run a positioned UPDATE or DELETE of a rowset of more than one row, or after a row FETCH from one", statement.0);
278        return Err(Abend { code: "EXEC".into(), message, pos: at.pos, file: None });
279    }
280    let position = current_of.map(|c| session(x).cursor(at.program, c).map(|c| c.positioned));
281    let values = match (position, values) {
282        (Some(None), _) => return Ok(Outcome::error(-507, "24501")),
283        (Some(Some(false)), _) => return Ok(Outcome::error(-508, "24504")),
284        (_, Err(e)) => return Ok(Outcome::error(e.code, e.state)),
285        (_, Ok(values)) => values,
286    };
287    let mut answer = database(x, &at.call(statement.0, current_of, statement.1, &values), |db, c| db.execute(c), at.pos)?;
288    if current_of.is_none() && answer.sqlcode == 0 && answer.affected == 0 {
289        answer = Outcome::error(100, "02000");
290    }
291    if let (Some(c), true, true) = (current_of, delete, answer.sqlcode >= 0)
292        && let Some(open) = session(x).cursor(at.program, c)
293    {
294        open.positioned = false;
295        open.held.clear();
296        open.current = 0;
297    }
298    Ok(answer)
299}
300/// The rows FOR n ROWS asks for, `implicit` without it; None where n is no integer of a row count.
301fn row_count<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, rows: &RowCount<P>, implicit: u32, refused: &dyn Fn(String) -> Abend) -> R<Option<u32>> {
302    let n = match rows {
303        RowCount::Implicit => return Ok(Some(implicit)),
304        RowCount::Constant(n) => i64::from(*n),
305        RowCount::Host(place) => match host::inputs(x, std::slice::from_ref(place))? {
306            Ok(values) => match values.first() {
307                Some(Value::Int(n)) => *n,
308                Some(Value::Decimal { value, scale: 0 }) => i64::try_from(*value).unwrap_or(i64::MAX),
309                _ => return Err(refused(ROWS_RULE.into())),
310            },
311            Err(_) => return Err(refused(ROWS_RULE.into())),
312        },
313    };
314    Ok(u32::try_from(n).ok().filter(|&k| (1..=MOST_ROWS).contains(&k)))
315}
316
317/// Whether `rows` rows fit every array (-246 where they do not).
318fn fits<'a, P: 'a>(rows: u32, arrays: impl IntoIterator<Item = &'a HostArray<P>>) -> bool {
319    arrays.into_iter().all(|a| a.array.is_none_or(|d| rows <= d.count))
320}
321
322/// FETCH NEXT ROWSET of `rows` rows: past the current rowset, the rows already read first, then
323/// the backend's. A rowset short of `rows` is +100 with its rows; elements after them are left as
324/// they were ([`numeric::assumptions::ROWSET_ENDS_SHORT`]).
325fn fetch_rowset<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, at: At, cursor: &str, text: &str, rows: u32, into: &[HostArray<P>], warnings: &mut Warnings) -> R<Outcome> {
326    let open = session(x).cursor(at.program, cursor).expect("the cursor is open");
327    open.held.drain(..open.current);
328    let mut fetched: Vec<Vec<Value>> = open.held.iter().take(rows as usize).cloned().collect();
329    let mut answer = Outcome::ok();
330    if fetched.len() < rows as usize {
331        let more = rows - fetched.len() as u32;
332        let count = [Value::Int(more.into())];
333        answer = database(x, &at.call("FETCH", Some(cursor), text, &count), |db, c| db.fetch_rows(c, more), at.pos)?;
334        if answer.rows.len() > more as usize {
335            return Err(Abend { code: "SQL".into(), message: format!("the database answered FETCH {cursor} for {more} rows with {}", answer.rows.len()), pos: at.pos, file: None });
336        }
337        let open = session(x).cursor(at.program, cursor).expect("the cursor is open");
338        open.held.extend(answer.rows.iter().cloned());
339        fetched.append(&mut answer.rows);
340    }
341    let open = session(x).cursor(at.program, cursor).expect("the cursor is open");
342    open.current = fetched.len();
343    open.rowset_size = Some(rows);
344    open.positioned = !fetched.is_empty();
345    let (assigned, failed) = if into.is_empty() { (fetched.len(), None) } else { host::assign_rows(x, into, &fetched, warnings)? };
346    let outcome = match failed {
347        Some(e) => Outcome::error(e.code, e.state),
348        None if answer.sqlcode != 0 => answer,
349        None if fetched.len() < rows as usize => Outcome::error(100, "02000"),
350        None => Outcome::ok(),
351    };
352    Ok(Outcome { affected: assigned as i64, rows: Vec::new(), ..outcome })
353}
354
355/// A CALL's answer: unless it is an error, each argument the procedure returns is assigned. +466
356/// says the procedure returned result sets, which SQLWARN9 shows with Z.
357fn returned<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, answer: Outcome, procedure: &str, args: &[HostPlace<P>], warnings: &mut Warnings, pos: Pos) -> R<Outcome> {
358    if answer.sqlcode < 0 || answer.parameters.is_empty() {
359        warnings[RESULT_SETS] = answer.sqlcode == 466;
360        return Ok(answer);
361    }
362    if answer.parameters.len() != args.len() {
363        return Err(Abend { code: "SQL".into(), message: format!("the database answered CALL {procedure} with {} arguments for its {}", answer.parameters.len(), args.len()), pos, file: None });
364    }
365    warnings[RESULT_SETS] = answer.sqlcode == 466;
366    Ok(match host::assign_returned(x, args, &answer.parameters, warnings)? {
367        Ok(()) => answer,
368        Err(e) => Outcome::error(e.code, e.state),
369    })
370}
371
372/// The statement string of a PREPARE or EXECUTE IMMEDIATE, normalised, after the input trace is
373/// told of it; None where it is not one varying-length string.
374fn 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>>> {
375    match source {
376        [place] if matches!(place.ty, Ok(super::HostType::VarChar(_) | super::HostType::VarGraphic(_)) | Err(_)) => {}
377        _ => return Ok(None),
378    }
379    Ok(Some(host::traced(x, source)?.map(|values| {
380        let string = values.first().map(Value::text).unwrap_or_default();
381        x.sink("dynamic-sql", pos, &string);
382        dynamic::normalise(&string)
383    })))
384}
385
386/// A dynamic statement that is not run by a cursor, as EXECUTE and EXECUTE IMMEDIATE run it.
387fn 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> {
388    let verb = dynamic::verb(text);
389    Ok(match dynamic::kind(text) {
390        Kind::Query => Outcome::error(-518, "07003"),
391        Kind::Unacceptable => Outcome::error(-84, "42612"),
392        Kind::Unknown => Outcome::error(-104, "42601"),
393        Kind::Refused(what) => {
394            let abend = refused(format!("ironwork for COBOL does not run {what}"));
395            return Err(Abend { message: crate::refusal::IWR0061.message(&abend.message), ..abend });
396        }
397        Kind::Commit => end_unit(x, at, text, true)?,
398        Kind::Rollback => end_unit(x, at, text, false)?,
399        Kind::Change { delete, current_of } => change(x, at, (&verb, text), values, delete, current_of.as_deref())?,
400        Kind::Other => match values {
401            Err(e) => Outcome::error(e.code, e.state),
402            Ok(values) => database(x, &at.call(&verb, None, text, &values), |db, c| db.execute(c), at.pos)?,
403        },
404    })
405}
406
407/// Where a statement's inputs come from: its host variables, or the SQLDA USING DESCRIPTOR names.
408enum Sources<'a, P> {
409    Hosts(&'a [HostPlace<P>]),
410    Descriptor(P),
411}
412
413/// Where a row goes: the INTO host variables, or those an SQLDA describes.
414enum Receivers<'a, P> {
415    Hosts(&'a [HostPlace<P>]),
416    Vars(&'a [Var]),
417}
418
419fn targets(vars: &[Var]) -> Vec<Target<'_>> {
420    vars.iter().map(|v| Target { offset: v.offset, len: v.len, ty: &v.ty, indicator: v.indicator }).collect()
421}
422
423/// SQLCODE -804, Db2's reason code its message token.
424fn invalid_sqlda(Invalid(reason): Invalid) -> Outcome {
425    Outcome { tokens: format!("{reason:02}"), ..Outcome::error(sqlda::INVALID.code, sqlda::INVALID.state) }
426}
427
428/// The values a dynamic statement's `markers` parameter markers take, or the outcome that refuses
429/// them: without markers USING is not read, and with them each needs one value (-313).
430fn marker_values<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, sources: Sources<P>, markers: usize, pos: Pos) -> R<Result<Result<Vec<Value>, SqlError>, Outcome>> {
431    if markers == 0 {
432        return Ok(Ok(Ok(Vec::new())));
433    }
434    match sources {
435        Sources::Hosts(hosts) if hosts.len() != markers => Ok(Err(Outcome::error(-313, "07001"))),
436        Sources::Hosts(hosts) => Ok(Ok(host::inputs(x, hosts)?)),
437        Sources::Descriptor(descriptor) => {
438            let at = x.locate(descriptor, false)?.offset;
439            match sqlda::vars(x.mem(), at, true) {
440                Err(invalid) => Ok(Err(invalid_sqlda(invalid))),
441                Ok(vars) if vars.len() != markers => Ok(Err(Outcome::error(-313, "07001"))),
442                Ok(vars) => Ok(Ok(host::read_targets(x, &targets(&vars), pos)?)),
443            }
444        }
445    }
446}
447
448/// PREPARE: the statement of the same name destroyed, then the new one made, unless it is an open
449/// cursor's (-519) or Db2 does not prepare it (-084).
450fn prepare<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, at: At, name: &str, string: Result<String, SqlError>) -> R<Outcome> {
451    let text = match string {
452        Err(e) => return Ok(Outcome::error(e.code, e.state)),
453        Ok(text) => text,
454    };
455    if session(x).running(at.program, name) {
456        return Ok(Outcome::error(-519, "24506"));
457    }
458    session(x).prepare(at.program, name, None);
459    let kind = dynamic::kind(&text);
460    match kind {
461        Kind::Unacceptable => return Ok(Outcome::error(-84, "42612")),
462        Kind::Unknown => return Ok(Outcome::error(-104, "42601")),
463        _ => {}
464    }
465    let mut answer = database(x, &at.call("PREPARE", Some(name), &text, &[]), |db, c| db.prepare(c), at.pos)?;
466    if answer.sqlcode >= 0 {
467        let markers = dynamic::markers(&text);
468        let columns = std::mem::take(&mut answer.columns);
469        session(x).prepare(at.program, name, Some(Prepared { text, query: kind == Kind::Query, markers, columns }));
470    }
471    Ok(answer)
472}
473
474/// DESCRIBE of a prepared statement into the SQLDA at `descriptor`, from what PREPARE's answer
475/// described. The database's description is input, as its rows are.
476fn describe<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, prepared: &Prepared, descriptor: P, names: SqlNames, pos: Pos) -> R<Outcome> {
477    let at = x.locate(descriptor, true)?.offset;
478    let page = x.facts().page();
479    let columns = prepared.query.then_some(prepared.columns.as_slice());
480    match sqlda::describe(x.mem(), at, columns, names, page) {
481        Ok(written) => {
482            if let Some(taint) = x.taint() {
483                taint.set(at, written, true);
484            }
485            Ok(Outcome::ok())
486        }
487        Err(Ok(invalid)) => Ok(invalid_sqlda(invalid)),
488        Err(Err(why)) => Err(Abend { code: "SQL".into(), message: format!("DESCRIBE was reached: {why}"), pos, file: None }),
489    }
490}
491
492fn execute<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, at: At, name: &str, sources: Sources<P>, refused: &dyn Fn(String) -> Abend) -> R<Outcome> {
493    let Some(p) = session(x).prepared(at.program, name).cloned().filter(|p| !p.query) else { return Ok(Outcome::error(-518, "07003")) };
494    match marker_values(x, sources, p.markers, at.pos)? {
495        Err(outcome) => Ok(outcome),
496        Ok(values) => dynamic_statement(x, at, &p.text, values, refused),
497    }
498}
499
500/// OPEN of a cursor for a prepared statement, `names` the cursor's and the statement's.
501fn open_prepared<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, at: At, names: (&str, &str), with_hold: bool, sources: Sources<P>) -> R<Outcome> {
502    let (cursor, statement) = names;
503    if session(x).cursor(at.program, cursor).is_some() {
504        return Ok(Outcome::error(-502, "24502"));
505    }
506    let p = match session(x).prepared(at.program, statement).cloned() {
507        None => return Ok(Outcome::error(-514, "26501")),
508        Some(Prepared { query: false, .. }) => return Ok(Outcome::error(-517, "07005")),
509        Some(p) => p,
510    };
511    let values = match marker_values(x, sources, p.markers, at.pos)? {
512        Err(outcome) => return Ok(outcome),
513        Ok(Err(e)) => return Ok(Outcome::error(e.code, e.state)),
514        Ok(Ok(values)) => values,
515    };
516    let hold = if with_hold { " WITH HOLD" } else { "" };
517    let text = format!("DECLARE {cursor} CURSOR{hold} FOR {}", p.text);
518    let answer = database(x, &at.call("OPEN", Some(cursor), &text, &values), |db, c| db.open(c), at.pos)?;
519    if answer.sqlcode >= 0 {
520        session(x).opened(at.program, cursor, with_hold, Some(statement));
521    }
522    Ok(answer)
523}
524
525/// FETCH, `statement` its cursor and text: one row into the receivers, the cursor on it.
526fn fetch<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, at: At, statement: (&str, &str), into: Receivers<P>, warnings: &mut Warnings) -> R<Outcome> {
527    let (cursor, text) = statement;
528    // A row FETCH moves from the first row of the current position, onto a row a rowset FETCH may
529    // have read already (C421).
530    let held = session(x).cursor(at.program, cursor).map(|open| {
531        if open.current > 0 {
532            open.held.pop_front();
533        }
534        open.current = 0;
535        open.rowset_size = None;
536        open.held.front().cloned()
537    });
538    let answer = match held {
539        None => return Ok(Outcome::error(-501, "24501")),
540        Some(Some(row)) => Outcome::rows(vec![row]),
541        Some(None) => database(x, &at.call("FETCH", Some(cursor), text, &[]), |db, c| db.fetch(c), at.pos)?,
542    };
543    if answer.rows.len() > 1 {
544        return Err(Abend { code: "SQL".into(), message: format!("the database answered FETCH {cursor} with {} rows", answer.rows.len()), pos: at.pos, file: None });
545    }
546    let on_row = answer.sqlcode >= 0 && answer.rows.len() == 1;
547    if let Some(open) = session(x).cursor(at.program, cursor) {
548        open.positioned = on_row;
549        if on_row {
550            if open.held.is_empty() {
551                open.held.push_back(answer.rows[0].clone());
552            }
553            open.current = 1;
554        }
555    }
556    let fetched = single_row(x, answer, into, warnings)?;
557    Ok(Outcome { affected: i64::from(on_row), ..fetched })
558}
559
560/// A SELECT INTO's or a FETCH's answer: one row is assigned, none is +100, more than one is -811.
561fn single_row<'w, P: Copy, S>(x: &mut impl SqlHost<'w, P, S>, answer: Outcome, into: Receivers<P>, warnings: &mut Warnings) -> R<Outcome> {
562    if answer.sqlcode < 0 {
563        return Ok(answer);
564    }
565    let assigned = |x: &mut _, row: &[Value], warnings: &mut Warnings| match into {
566        Receivers::Hosts(hosts) => host::assign(x, hosts, row, warnings),
567        Receivers::Vars(vars) => host::assign_targets(x, &targets(vars), row, warnings),
568    };
569    Ok(match answer.rows.len() {
570        0 => Outcome::error(100, "02000"),
571        1 => match assigned(x, &answer.rows[0], warnings)? {
572            Ok(()) => answer,
573            Err(e) => Outcome::error(e.code, e.state),
574        },
575        _ => Outcome::error(-811, "21000"),
576    })
577}