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