Skip to main content

ironwork_syntax/
sql.rs

1//! EXEC SQL statements, typed: what each one sends, what it receives into, and the WHENEVER actions
2//! in force where it stands in the listing. The text a backend runs is the statement with each
3//! input host variable replaced by `?` and any INTO list removed, in one canonical spelling.
4
5use crate::Pos;
6use crate::ast::{Expr, Literal, Operand, Ref};
7use std::collections::HashMap;
8
9/// A host variable and the indicator variable that follows it.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct HostVar {
12    pub var: Ref,
13    pub indicator: Option<Ref>,
14    /// Written with its colon; an INTO list may name a host variable without one.
15    pub colon: bool,
16}
17
18#[derive(Clone, Debug, Default, PartialEq, Eq)]
19pub enum Action {
20    #[default]
21    Continue,
22    GoTo(String),
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum Condition {
27    SqlError,
28    NotFound,
29    SqlWarning,
30}
31
32/// The WHENEVER actions in force at a statement.
33#[derive(Clone, Debug, Default, PartialEq, Eq)]
34pub struct Whenever {
35    pub sqlerror: Action,
36    pub not_found: Action,
37    pub sqlwarning: Action,
38}
39
40impl Whenever {
41    pub fn set(&mut self, condition: Condition, action: Action) {
42        match condition {
43            Condition::SqlError => self.sqlerror = action,
44            Condition::NotFound => self.not_found = action,
45            Condition::SqlWarning => self.sqlwarning = action,
46        }
47    }
48}
49
50/// What DESCRIBE puts in each SQLNAME: USING NAMES, LABELS or ANY.
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub enum Names {
53    #[default]
54    Names,
55    Labels,
56    Any,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum ChangeKind {
61    Insert,
62    Update,
63    Delete,
64}
65
66/// A cursor as its DECLARE gives it: the query OPEN runs and the host variables OPEN sends, or
67/// the prepared statement it runs, whose inputs OPEN ... USING sends.
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct Cursor {
70    pub name: String,
71    pub text: String,
72    pub inputs: Vec<HostVar>,
73    pub with_hold: bool,
74    /// The statement name of a cursor for a prepared statement; its `text` and `inputs` are empty.
75    pub statement: Option<String>,
76    /// WITH ROWSET POSITIONING, which a rowset FETCH needs.
77    pub rowset: bool,
78}
79
80/// FOR n ROWS: absent, so a rowset FETCH asks for as many rows as the cursor's last one did, or
81/// one; a constant; or a host variable.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub enum Rows {
84    Implicit,
85    Constant(u32),
86    Host(Box<HostVar>),
87}
88
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub enum Statement {
91    /// SELECT ... INTO, VALUES ... INTO, and SET :host-variable = expression.
92    Query { text: String, inputs: Vec<HostVar>, into: Vec<HostVar> },
93    /// INSERT, UPDATE and DELETE; `current_of` names the cursor of a positioned UPDATE or DELETE.
94    Change { kind: ChangeKind, text: String, inputs: Vec<HostVar>, current_of: Option<String> },
95    DeclareCursor(Cursor),
96    /// A cursor ironwork does not run, such as a scrollable one, and what it is.
97    DeclareUnsupported { name: String, what: String },
98    /// `declared` is the cursor's DECLARE, which [`Cursors::resolve`] fills; `using` the host
99    /// variables OPEN ... USING sends to a prepared statement's parameter markers, or `descriptor`
100    /// the SQLDA OPEN ... USING DESCRIPTOR names.
101    Open { cursor: String, declared: Option<Cursor>, using: Vec<HostVar>, descriptor: Option<HostVar> },
102    Fetch { cursor: String, into: Vec<HostVar> },
103    FetchDescriptor { cursor: String, descriptor: HostVar },
104    /// FETCH NEXT ROWSET into host-variable arrays; `enabled` is whether the cursor's DECLARE
105    /// allows rowsets, which [`Cursors::resolve`] fills.
106    FetchRowset { cursor: String, rows: Rows, into: Vec<HostVar>, enabled: bool },
107    Close { cursor: String },
108    /// INSERT ... FOR n ROWS: `text` and `inputs` are the INSERT of one row, which runs for each.
109    InsertRows { text: String, inputs: Vec<HostVar>, rows: Rows, atomic: bool },
110    /// CALL of a stored procedure: `text` names it with `?` for each host-variable argument.
111    Call { procedure: String, text: String, args: Vec<HostVar> },
112    Commit,
113    Rollback,
114    /// PREPARE: the statement name, the host variable holding the statement string, and the SQLDA
115    /// PREPARE ... INTO describes the statement in.
116    Prepare { name: String, source: HostVar, into: Option<(HostVar, Names)> },
117    ExecuteImmediate { source: HostVar },
118    /// EXECUTE of a prepared statement, with the host variables USING sends or the SQLDA USING
119    /// DESCRIPTOR names.
120    Execute { name: String, inputs: Vec<HostVar>, descriptor: Option<HostVar> },
121    /// DESCRIBE [OUTPUT] of a prepared statement into an SQLDA.
122    Describe { name: String, descriptor: HostVar, names: Names },
123    Whenever { condition: Condition, action: Action },
124    /// INCLUDE, DECLARE SECTION, DECLARE TABLE and DECLARE STATEMENT, which declare and do nothing.
125    Declaration,
126    /// CONNECT and SET CONNECTION, which ironwork does not run: `what` names the statement, and
127    /// `target` is the host variable naming the location, where one does.
128    Connect { what: String, target: Option<HostVar> },
129    /// A statement ironwork does not run, named by what it is.
130    Unsupported(String),
131    /// A statement the precompiler refuses, with the reason: it does not read as its verb requires,
132    /// or it is not Db2 for z/OS.
133    Malformed(String),
134}
135
136impl Statement {
137    /// Every host variable and indicator the statement names.
138    pub fn references(&self) -> Vec<&Ref> {
139        let vars: Vec<&HostVar> = match self {
140            Statement::Query { inputs, into, .. } => into.iter().chain(inputs).collect(),
141            Statement::Change { inputs, .. } | Statement::DeclareCursor(Cursor { inputs, .. }) => inputs.iter().collect(),
142            Statement::Fetch { into, .. } => into.iter().collect(),
143            Statement::FetchRowset { rows, into, .. } => into.iter().chain(rows.host()).collect(),
144            Statement::InsertRows { inputs, rows, .. } => inputs.iter().chain(rows.host()).collect(),
145            Statement::Call { args, .. } => args.iter().collect(),
146            Statement::Open { using, descriptor, .. } | Statement::Execute { inputs: using, descriptor, .. } => using.iter().chain(descriptor).collect(),
147            Statement::Prepare { source, into, .. } => std::iter::once(source).chain(into.as_ref().map(|(d, _)| d)).collect(),
148            Statement::ExecuteImmediate { source } | Statement::FetchDescriptor { descriptor: source, .. } | Statement::Describe { descriptor: source, .. } => vec![source],
149            _ => Vec::new(),
150        };
151        vars.into_iter().flat_map(|h| std::iter::once(&h.var).chain(&h.indicator)).collect()
152    }
153}
154
155impl Rows {
156    fn host(&self) -> Option<&HostVar> {
157        match self {
158            Rows::Host(h) => Some(h.as_ref()),
159            _ => None,
160        }
161    }
162}
163
164/// The cursors declared so far in a listing. The precompiler reads a cursor's name against the
165/// DECLAREs before it, so a statement naming a cursor declared later, or never, is refused.
166#[derive(Debug, Default)]
167pub struct Cursors(HashMap<String, Result<Cursor, String>>);
168
169impl Cursors {
170    /// Records a DECLARE CURSOR, gives OPEN its cursor, and refuses a cursor not yet declared.
171    pub fn resolve(&mut self, statement: Statement) -> Statement {
172        let named = match &statement {
173            Statement::DeclareCursor(c) => {
174                self.0.insert(c.name.clone(), Ok(c.clone()));
175                return statement;
176            }
177            Statement::DeclareUnsupported { name, what } => {
178                self.0.insert(name.clone(), Err(what.clone()));
179                return statement;
180            }
181            Statement::Open { cursor, .. }
182            | Statement::Fetch { cursor, .. }
183            | Statement::FetchDescriptor { cursor, .. }
184            | Statement::FetchRowset { cursor, .. }
185            | Statement::Close { cursor, .. }
186            | Statement::Change { current_of: Some(cursor), .. } => cursor.clone(),
187            _ => return statement,
188        };
189        match (self.0.get(&named), statement) {
190            (None, _) => Statement::Malformed(format!("cursor {named} is not declared before this statement")),
191            (Some(Err(what)), _) => Statement::Unsupported(what.clone()),
192            (Some(Ok(c)), Statement::Open { using, descriptor, .. }) if c.statement.is_none() && (!using.is_empty() || descriptor.is_some()) => {
193                Statement::Unsupported("OPEN ... USING of a cursor declared for a select-statement".into())
194            }
195            (Some(Ok(c)), Statement::Open { cursor, using, descriptor, .. }) => Statement::Open { cursor, declared: Some(c.clone()), using, descriptor },
196            (Some(Ok(c)), Statement::FetchRowset { cursor, rows, into, .. }) => Statement::FetchRowset { cursor, rows, into, enabled: c.rowset },
197            (Some(Ok(_)), statement) => statement,
198        }
199    }
200}
201
202/// An EXEC SQL block's typed statement, its place among the program's EXEC SQL blocks in listing
203/// order (from 1), and the WHENEVER actions in force where it stands.
204#[derive(Clone, Debug, PartialEq, Eq)]
205pub struct Sql {
206    pub statement: Statement,
207    pub ordinal: u32,
208    pub whenever: Whenever,
209}
210
211pub use rt::sql::fingerprint;
212
213impl Statement {
214    /// The names in an INTO list written without their colon.
215    pub fn colonless_into(&self) -> impl Iterator<Item = &Ref> {
216        let into: &[HostVar] = match self {
217            Statement::Query { into, .. } | Statement::Fetch { into, .. } | Statement::FetchRowset { into, .. } => into,
218            _ => &[],
219        };
220        into.iter().filter(|h| !h.colon).map(|h| &h.var)
221    }
222}
223
224/// The statement in `body`, the text between EXEC SQL and END-EXEC.
225pub fn parse(body: &str, pos: Pos) -> Statement {
226    match lex(body) {
227        Ok(toks) => statement(&toks, pos),
228        Err(why) => Statement::Malformed(why),
229    }
230}
231
232#[derive(Clone, Debug, PartialEq, Eq)]
233enum Tok {
234    Word(String),
235    Quoted(String),
236    Number(String),
237    Punct(char),
238    Host { path: Vec<String>, subscripts: Vec<String> },
239}
240
241fn lex(sql: &str) -> Result<Vec<Tok>, String> {
242    let chars: Vec<char> = sql.chars().collect();
243    let (mut out, mut i) = (Vec::new(), 0);
244    let text = |from: usize, to: usize| chars[from..to].iter().collect::<String>();
245    while let Some(&c) = chars.get(i) {
246        if c.is_whitespace() {
247            i += 1;
248        } else if c == '\'' || c == '"' {
249            let start = i;
250            i += 1;
251            loop {
252                match chars.get(i) {
253                    None => return Err("a quoted string is not closed".into()),
254                    Some(&q) if q == c && chars.get(i + 1) == Some(&c) => i += 2,
255                    Some(&q) if q == c => {
256                        i += 1;
257                        break;
258                    }
259                    Some(_) => i += 1,
260                }
261            }
262            out.push(Tok::Quoted(text(start, i)));
263        } else if c == ':' && chars.get(i + 1).is_some_and(char::is_ascii_alphanumeric) {
264            i += 1;
265            let mut path = Vec::new();
266            loop {
267                let start = i;
268                while chars.get(i).is_some_and(|&n| n.is_ascii_alphanumeric() || n == '-' || n == '_') {
269                    i += 1;
270                }
271                path.push(text(start, i).to_ascii_uppercase());
272                if chars.get(i) == Some(&'.') && chars.get(i + 1).is_some_and(char::is_ascii_alphanumeric) {
273                    i += 1;
274                } else {
275                    break;
276                }
277            }
278            let mut subscripts = Vec::new();
279            if chars.get(i) == Some(&'(') {
280                let close = chars[i..].iter().position(|&n| n == ')').ok_or("a subscript is not closed")? + i;
281                subscripts = text(i + 1, close).split(',').map(|s| s.trim().to_ascii_uppercase()).collect();
282                if subscripts.iter().any(String::is_empty) {
283                    return Err("a subscript is empty".into());
284                }
285                i = close + 1;
286            }
287            out.push(Tok::Host { path, subscripts });
288        } else if c.is_ascii_alphabetic() || matches!(c, '_' | '#' | '@' | '$') {
289            // Cursor and statement names in COBOL programs take hyphens, as in PROGRAMS-CSR.
290            let start = i;
291            while chars.get(i).is_some_and(|&n| {
292                n.is_ascii_alphanumeric() || matches!(n, '_' | '#' | '@' | '$') || (n == '-' && chars.get(i + 1).is_some_and(char::is_ascii_alphanumeric))
293            }) {
294                i += 1;
295            }
296            out.push(Tok::Word(text(start, i).to_ascii_uppercase()));
297        } else if c.is_ascii_digit() {
298            let start = i;
299            while chars.get(i).is_some_and(|&n| n.is_ascii_digit() || n == '.') {
300                i += 1;
301            }
302            out.push(Tok::Number(text(start, i)));
303        } else {
304            out.push(Tok::Punct(c));
305            i += 1;
306        }
307    }
308    Ok(out)
309}
310
311fn word(toks: &[Tok], i: usize) -> &str {
312    match toks.get(i) {
313        Some(Tok::Word(w)) => w,
314        _ => "",
315    }
316}
317
318fn reference(path: &[String], subscripts: &[String], pos: Pos) -> Ref {
319    let mut qualifiers = path.to_vec();
320    let name = qualifiers.pop().unwrap_or_default();
321    qualifiers.reverse();
322    let subscripts = subscripts
323        .iter()
324        .map(|s| {
325            Expr::Operand(if s.chars().all(|c| c.is_ascii_digit()) {
326                Operand::Literal(Literal::Number(s.clone()))
327            } else {
328                Operand::Ref(Ref { name: s.clone(), qualifiers: Vec::new(), subscripts: Vec::new(), refmod: None, pos })
329            })
330        })
331        .collect();
332    Ref { name, qualifiers, subscripts, refmod: None, pos }
333}
334
335/// The host variable starting at `toks[i]`, with the indicator after it, and where it ends.
336fn host_var(toks: &[Tok], i: usize, pos: Pos) -> Option<(HostVar, usize)> {
337    let Some(Tok::Host { path, subscripts }) = toks.get(i) else { return None };
338    let var = reference(path, subscripts, pos);
339    let at = if word(toks, i + 1) == "INDICATOR" { i + 2 } else { i + 1 };
340    match toks.get(at) {
341        Some(Tok::Host { path, subscripts }) => Some((HostVar { var, indicator: Some(reference(path, subscripts, pos)), colon: true }, at + 1)),
342        _ => Some((HostVar { var, indicator: None, colon: true }, i + 1)),
343    }
344}
345
346/// The canonical text of `toks`, each host variable replaced by `?`, and those host variables.
347fn render(toks: &[Tok], pos: Pos) -> (String, Vec<HostVar>) {
348    let (mut pieces, mut vars, mut i) = (Vec::<String>::new(), Vec::new(), 0);
349    while i < toks.len() {
350        if let Some((var, next)) = host_var(toks, i, pos) {
351            vars.push(var);
352            pieces.push("?".into());
353            i = next;
354            continue;
355        }
356        pieces.push(match &toks[i] {
357            Tok::Word(s) | Tok::Quoted(s) | Tok::Number(s) => s.clone(),
358            Tok::Punct(c) => c.to_string(),
359            Tok::Host { .. } => unreachable!("host_var takes every host token"),
360        });
361        i += 1;
362    }
363    let mut text = String::new();
364    for (n, piece) in pieces.iter().enumerate() {
365        let operator = n > 0 && matches!((pieces[n - 1].as_str(), piece.as_str()), ("<" | ">" | "!" | "¬" | "^", "=") | ("<", ">") | ("|", "|"));
366        let glued = n == 0 || operator || matches!(piece.as_str(), ")" | "," | ".") || matches!(pieces[n - 1].as_str(), "(" | ".");
367        if !glued {
368            text.push(' ');
369        }
370        text.push_str(piece);
371    }
372    (text, vars)
373}
374
375/// A comma-separated list of host variables, as INTO and FETCH ... INTO take.
376fn host_list(toks: &[Tok], pos: Pos) -> Result<Vec<HostVar>, String> {
377    let (mut out, mut i) = (Vec::new(), 0);
378    while i < toks.len() {
379        // A name written without its colon is read as a host variable, which the compiler refuses
380        // under strict, as Db2's precompiler does, and accepts under extended (assumption S7).
381        let (var, next) = match toks.get(i) {
382            Some(Tok::Word(name)) => (HostVar { var: reference(std::slice::from_ref(name), &[], pos), indicator: None, colon: false }, i + 1),
383            _ => host_var(toks, i, pos).ok_or("INTO lists something other than host variables")?,
384        };
385        out.push(var);
386        i = next;
387        match toks.get(i) {
388            None => {}
389            Some(Tok::Punct(',')) => i += 1,
390            Some(_) => return Err("INTO lists something other than host variables".into()),
391        }
392    }
393    if out.is_empty() {
394        return Err("INTO names no host variable".into());
395    }
396    Ok(out)
397}
398
399/// Where `target` first stands outside parentheses, from `from`.
400fn top_level(toks: &[Tok], from: usize, target: &str) -> Option<usize> {
401    let mut depth = 0i32;
402    for (i, t) in toks.iter().enumerate().skip(from) {
403        match t {
404            Tok::Punct('(') => depth += 1,
405            Tok::Punct(')') => depth -= 1,
406            Tok::Word(w) if depth == 0 && w == target => return Some(i),
407            _ => {}
408        }
409    }
410    None
411}
412
413fn statement(toks: &[Tok], pos: Pos) -> Statement {
414    let verb = word(toks, 0);
415    match verb {
416        "SELECT" | "VALUES" => into_query(toks, pos),
417        "SET" if matches!(toks.get(1), Some(Tok::Host { .. })) => set_host(toks, pos),
418        "SET" if word(toks, 1) == "CONNECTION" => connect("SET CONNECTION", toks, 2, pos),
419        "CONNECT" if word(toks, 1) == "TO" => connect("CONNECT", toks, 2, pos),
420        "CONNECT" => Statement::Connect { what: "CONNECT".into(), target: None },
421        "INSERT" => insert(toks, pos),
422        "UPDATE" => change(ChangeKind::Update, toks, pos),
423        "DELETE" => change(ChangeKind::Delete, toks, pos),
424        "DECLARE" => declare(toks, pos),
425        "OPEN" => open(toks, pos),
426        "FETCH" => fetch(toks, pos),
427        "CLOSE" => match (word(toks, 1), toks.len()) {
428            ("", _) => Statement::Malformed("CLOSE names no cursor".into()),
429            (cursor, 2) => Statement::Close { cursor: cursor.into() },
430            _ => Statement::Malformed("CLOSE takes only a cursor name".into()),
431        },
432        "COMMIT" if toks.len() == 1 || (toks.len() == 2 && word(toks, 1) == "WORK") => Statement::Commit,
433        "ROLLBACK" if toks.len() == 1 || (toks.len() == 2 && word(toks, 1) == "WORK") => Statement::Rollback,
434        "ROLLBACK" => Statement::Unsupported("ROLLBACK TO SAVEPOINT".into()),
435        "PREPARE" => prepare(toks, pos),
436        "EXECUTE" if word(toks, 1) == "IMMEDIATE" => match source(toks, 2, pos) {
437            Ok(source) => Statement::ExecuteImmediate { source },
438            Err(why) => Statement::Malformed(format!("EXECUTE IMMEDIATE {why}")),
439        },
440        "EXECUTE" => execute(toks, pos),
441        "DESCRIBE" => describe(toks, pos),
442        "CALL" => call(toks, pos),
443        "WHENEVER" => whenever(toks),
444        "INCLUDE" => Statement::Declaration,
445        "BEGIN" | "END" if word(toks, 1) == "DECLARE" => Statement::Declaration,
446        "DISCONNECT" => Statement::Malformed("DISCONNECT is not a Db2 for z/OS statement; Db2 ends a connection with RELEASE and a commit".into()),
447        "" => Statement::Malformed("the block holds no statement".into()),
448        other => Statement::Unsupported(other.into()),
449    }
450}
451
452/// CONNECT TO or SET CONNECTION, its location at `toks[at]`: a host variable, or a name.
453fn connect(what: &str, toks: &[Tok], at: usize, pos: Pos) -> Statement {
454    Statement::Connect { what: what.into(), target: host_var(toks, at, pos).map(|(var, _)| var) }
455}
456
457fn into_query(toks: &[Tok], pos: Pos) -> Statement {
458    let Some(into) = top_level(toks, 1, "INTO") else {
459        return Statement::Malformed(format!("{} has no INTO", word(toks, 0)));
460    };
461    let end = top_level(toks, into + 1, "FROM").unwrap_or(toks.len());
462    let into_vars = match host_list(&toks[into + 1..end], pos) {
463        Ok(v) => v,
464        Err(why) => return Statement::Malformed(why),
465    };
466    let rest: Vec<Tok> = toks[..into].iter().chain(&toks[end..]).cloned().collect();
467    let (text, inputs) = render(&rest, pos);
468    Statement::Query { text, inputs, into: into_vars }
469}
470
471fn set_host(toks: &[Tok], pos: Pos) -> Statement {
472    let Some((target, next)) = host_var(toks, 1, pos) else { unreachable!("statement checks the host variable") };
473    if toks.get(next) != Some(&Tok::Punct('=')) || next + 1 >= toks.len() {
474        return Statement::Malformed("SET :host-variable takes = and an expression".into());
475    }
476    let expression: Vec<Tok> = std::iter::once(Tok::Word("VALUES".into())).chain(toks[next + 1..].iter().cloned()).collect();
477    let (text, inputs) = render(&expression, pos);
478    Statement::Query { text, inputs, into: vec![target] }
479}
480
481fn change(kind: ChangeKind, toks: &[Tok], pos: Pos) -> Statement {
482    let n = toks.len();
483    if n >= 5 && (word(toks, n - 5), word(toks, n - 4), word(toks, n - 2), word(toks, n - 1)) == ("FOR", "ROW", "OF", "ROWSET") {
484        return Statement::Unsupported("a positioned UPDATE or DELETE FOR ROW n OF ROWSET".into());
485    }
486    let current_of = (n >= 4 && word(toks, n - 4) == "WHERE" && word(toks, n - 3) == "CURRENT" && word(toks, n - 2) == "OF" && !word(toks, n - 1).is_empty())
487        .then(|| word(toks, n - 1).to_owned());
488    let (text, inputs) = render(toks, pos);
489    Statement::Change { kind, text, inputs, current_of }
490}
491
492fn declare(toks: &[Tok], pos: Pos) -> Statement {
493    let name = word(toks, 1);
494    match word(toks, 2) {
495        "TABLE" | "STATEMENT" => return Statement::Declaration,
496        _ if name.is_empty() => return Statement::Malformed("DECLARE names nothing".into()),
497        _ => {}
498    }
499    let Some(cursor) = (2..toks.len()).find(|&i| word(toks, i) == "CURSOR") else {
500        return Statement::Unsupported(format!("DECLARE {}", word(toks, 2)));
501    };
502    let unsupported = |what: &str| Statement::DeclareUnsupported { name: name.into(), what: what.into() };
503    if (2..cursor).any(|i| word(toks, i) == "SCROLL" && word(toks, i - 1) != "NO") {
504        return unsupported("a scrollable cursor");
505    }
506    let Some(for_at) = top_level(toks, cursor + 1, "FOR") else {
507        return Statement::Malformed("DECLARE CURSOR has no FOR".into());
508    };
509    let with_hold = (cursor + 1..for_at).any(|i| word(toks, i) == "HOLD" && word(toks, i - 1) == "WITH");
510    let rowset = (cursor + 1..for_at).any(|i| word(toks, i) == "ROWSET" && word(toks, i - 1) == "WITH");
511    let query = &toks[for_at + 1..];
512    if !matches!(word(query, 0), "SELECT" | "WITH" | "VALUES") && !matches!(query.first(), Some(Tok::Punct('('))) {
513        return match query {
514            [Tok::Word(statement)] => Statement::DeclareCursor(Cursor { name: name.into(), text: String::new(), inputs: Vec::new(), with_hold, statement: Some(statement.clone()), rowset }),
515            _ => Statement::Malformed("DECLARE CURSOR ... FOR takes a select-statement or a statement name".into()),
516        };
517    }
518    let (text, inputs) = render(query, pos);
519    Statement::DeclareCursor(Cursor { name: name.into(), text, inputs, with_hold, statement: None, rowset })
520}
521
522/// A comma-separated list of host variables after USING.
523fn using_list(toks: &[Tok], pos: Pos) -> Result<Vec<HostVar>, String> {
524    let (mut out, mut i) = (Vec::new(), 0);
525    while i < toks.len() {
526        let (var, next) = host_var(toks, i, pos).ok_or("USING lists something other than host variables")?;
527        out.push(var);
528        i = next;
529        match toks.get(i) {
530            None => {}
531            Some(Tok::Punct(',')) if i + 1 < toks.len() => i += 1,
532            Some(_) => return Err("USING lists something other than host variables".into()),
533        }
534    }
535    if out.is_empty() {
536        return Err("USING names no host variable".into());
537    }
538    Ok(out)
539}
540
541/// OPEN, and the host variables OPEN ... USING sends.
542fn open(toks: &[Tok], pos: Pos) -> Statement {
543    let cursor = word(toks, 1);
544    if cursor.is_empty() {
545        return Statement::Malformed("OPEN names no cursor".into());
546    }
547    match (toks.len(), word(toks, 2), word(toks, 3)) {
548        (2, _, _) => Statement::Open { cursor: cursor.into(), declared: None, using: Vec::new(), descriptor: None },
549        (_, "USING", "DESCRIPTOR") => match descriptor(toks, 4, pos) {
550            Ok((d, next)) if next == toks.len() => Statement::Open { cursor: cursor.into(), declared: None, using: Vec::new(), descriptor: Some(d) },
551            Ok(_) => Statement::Malformed("OPEN ... USING DESCRIPTOR takes only the descriptor".into()),
552            Err(why) => Statement::Malformed(why),
553        },
554        (_, "USING", _) => match using_list(&toks[3..], pos) {
555            Ok(using) => Statement::Open { cursor: cursor.into(), declared: None, using, descriptor: None },
556            Err(why) => Statement::Malformed(why),
557        },
558        _ => Statement::Malformed("OPEN takes a cursor name and USING".into()),
559    }
560}
561
562/// The statement string's host variable at `toks[at]`, the statement's last token. Db2 takes no
563/// indicator with it, and a string expression only in PL/I (Db2 13 SQL, PREPARE).
564fn source(toks: &[Tok], at: usize, pos: Pos) -> Result<HostVar, String> {
565    match (toks.get(at), host_var(toks, at, pos)) {
566        (Some(Tok::Quoted(_)), _) => Err("takes a host variable; a string expression is PL/I's".into()),
567        (_, Some((HostVar { indicator: Some(_), .. }, _))) => Err("takes no indicator variable with the statement string".into()),
568        (_, Some((var, next))) if next == toks.len() => Ok(var),
569        _ => Err("takes the host variable holding the statement string".into()),
570    }
571}
572
573fn prepare(toks: &[Tok], pos: Pos) -> Statement {
574    let name = word(toks, 1);
575    if name.is_empty() {
576        return Statement::Malformed("PREPARE names no statement".into());
577    }
578    let (into, from) = match word(toks, 2) {
579        "INTO" => match descriptor(toks, 3, pos).and_then(|(d, next)| names(toks, next).map(|(n, next)| (d, n, next))) {
580            Ok((_, None, _)) => return Statement::Unsupported("PREPARE ... INTO ... USING BOTH".into()),
581            Ok((d, Some(n), next)) => (Some((d, n)), next),
582            Err(why) => return Statement::Malformed(why),
583        },
584        _ => (None, 2),
585    };
586    match word(toks, from) {
587        "ATTRIBUTES" => Statement::Unsupported("PREPARE ... ATTRIBUTES".into()),
588        "FROM" => match source(toks, from + 1, pos) {
589            Ok(source) => Statement::Prepare { name: name.into(), source, into },
590            Err(why) => Statement::Malformed(format!("PREPARE ... FROM {why}")),
591        },
592        _ => Statement::Malformed("PREPARE takes a statement name and FROM".into()),
593    }
594}
595
596/// The SQLDA host variable at `toks[at]`, which takes no indicator, and where it ends.
597fn descriptor(toks: &[Tok], at: usize, pos: Pos) -> Result<(HostVar, usize), String> {
598    match host_var(toks, at, pos) {
599        Some((HostVar { indicator: Some(_), .. }, _)) => Err("a descriptor takes no indicator variable".into()),
600        Some((d, next)) => Ok((d, next)),
601        None => Err("DESCRIPTOR and INTO name the SQLDA as a host variable".into()),
602    }
603}
604
605/// An optional USING NAMES, LABELS, ANY or BOTH from `toks[at]`: None for BOTH, which ironwork does
606/// not run, and where it ends.
607fn names(toks: &[Tok], at: usize) -> Result<(Option<Names>, usize), String> {
608    if word(toks, at) != "USING" {
609        return Ok((Some(Names::Names), at));
610    }
611    let names = match word(toks, at + 1) {
612        "NAMES" => Some(Names::Names),
613        "LABELS" => Some(Names::Labels),
614        "ANY" => Some(Names::Any),
615        "BOTH" => None,
616        _ => return Err("USING takes NAMES, LABELS, ANY or BOTH".into()),
617    };
618    Ok((names, at + 2))
619}
620
621/// DESCRIBE [OUTPUT] statement-name INTO descriptor [USING ...].
622fn describe(toks: &[Tok], pos: Pos) -> Statement {
623    let at = match word(toks, 1) {
624        "OUTPUT" => 2,
625        "INPUT" => return Statement::Unsupported("DESCRIBE INPUT".into()),
626        "CURSOR" | "PROCEDURE" | "TABLE" => return Statement::Unsupported(format!("DESCRIBE {}", word(toks, 1))),
627        _ => 1,
628    };
629    let name = word(toks, at);
630    if name.is_empty() || word(toks, at + 1) != "INTO" {
631        return Statement::Malformed("DESCRIBE takes a statement name and INTO".into());
632    }
633    match descriptor(toks, at + 2, pos).and_then(|(d, next)| names(toks, next).map(|(n, next)| (d, n, next))) {
634        Ok((_, None, _)) => Statement::Unsupported("DESCRIBE ... USING BOTH".into()),
635        Ok((descriptor, Some(names), next)) if next == toks.len() => Statement::Describe { name: name.into(), descriptor, names },
636        Ok(_) => Statement::Malformed("DESCRIBE ends with INTO and USING".into()),
637        Err(why) => Statement::Malformed(why),
638    }
639}
640
641fn execute(toks: &[Tok], pos: Pos) -> Statement {
642    let name = word(toks, 1);
643    if name.is_empty() {
644        return Statement::Malformed("EXECUTE names no statement".into());
645    }
646    match (toks.len(), word(toks, 2), word(toks, 3)) {
647        (2, _, _) => Statement::Execute { name: name.into(), inputs: Vec::new(), descriptor: None },
648        (_, "USING", "DESCRIPTOR") if top_level(toks, 4, "FOR").is_none() => match descriptor(toks, 4, pos) {
649            Ok((d, next)) if next == toks.len() => Statement::Execute { name: name.into(), inputs: Vec::new(), descriptor: Some(d) },
650            Ok(_) => Statement::Malformed("EXECUTE ... USING DESCRIPTOR takes only the descriptor".into()),
651            Err(why) => Statement::Malformed(why),
652        },
653        (_, "USING", _) if top_level(toks, 3, "FOR").is_none() => match using_list(&toks[3..], pos) {
654            Ok(inputs) => Statement::Execute { name: name.into(), inputs, descriptor: None },
655            Err(why) => Statement::Malformed(why),
656        },
657        (_, "USING" | "FOR", _) => Statement::Unsupported("a multi-row EXECUTE".into()),
658        _ => Statement::Malformed("EXECUTE takes a statement name and USING".into()),
659    }
660}
661
662/// A FETCH: row-positioned NEXT, or NEXT ROWSET, the one rowset orientation a cursor that does
663/// not scroll takes (Db2 13 SQL, FETCH).
664fn fetch(toks: &[Tok], pos: Pos) -> Statement {
665    let rowset = word(toks, 1) == "NEXT" && word(toks, 2) == "ROWSET";
666    let mut i = if rowset { 3 } else { 1 };
667    if !rowset && word(toks, i) == "NEXT" {
668        i += 1;
669    }
670    if matches!(word(toks, i), "PRIOR" | "FIRST" | "LAST" | "ABSOLUTE" | "RELATIVE" | "BEFORE" | "AFTER" | "CURRENT" | "SENSITIVE" | "INSENSITIVE" | "ROWSET") {
671        return Statement::Unsupported("a scrollable FETCH".into());
672    }
673    if word(toks, i) == "FROM" {
674        i += 1;
675    }
676    let cursor = word(toks, i);
677    if cursor.is_empty() {
678        return Statement::Malformed("FETCH names no cursor".into());
679    }
680    i += 1;
681    if rowset {
682        return fetch_rowset(cursor, &toks[i..], pos);
683    }
684    match word(toks, i) {
685        "" if i == toks.len() => Statement::Fetch { cursor: cursor.into(), into: Vec::new() },
686        "INTO" => match host_list(&toks[i + 1..], pos) {
687            Ok(into) => Statement::Fetch { cursor: cursor.into(), into },
688            Err(why) => Statement::Malformed(why),
689        },
690        "FOR" => Statement::Malformed("FOR n ROWS takes the rowset orientation NEXT ROWSET".into()),
691        "USING" if word(toks, i + 1) == "DESCRIPTOR" => match descriptor(toks, i + 2, pos) {
692            Ok((descriptor, next)) if next == toks.len() => Statement::FetchDescriptor { cursor: cursor.into(), descriptor },
693            Ok(_) => Statement::Malformed("FETCH ... USING DESCRIPTOR takes only the descriptor".into()),
694            Err(why) => Statement::Malformed(why),
695        },
696        _ => Statement::Malformed("FETCH takes a cursor and INTO".into()),
697    }
698}
699
700/// FETCH NEXT ROWSET's clauses after the cursor: FOR n ROWS, then INTO.
701fn fetch_rowset(cursor: &str, toks: &[Tok], pos: Pos) -> Statement {
702    let (rows, at) = match word(toks, 0) {
703        "FOR" => match rows_clause(toks, pos) {
704            Ok(clause) => clause,
705            Err(why) => return Statement::Malformed(why),
706        },
707        _ => (Rows::Implicit, 0),
708    };
709    let rest = &toks[at..];
710    let into = match (word(rest, 0), word(rest, 1)) {
711        _ if rest.is_empty() => Vec::new(),
712        ("INTO" | "USING", "DESCRIPTOR") => return Statement::Unsupported("FETCH ... INTO DESCRIPTOR".into()),
713        ("INTO", _) => match host_list(&rest[1..], pos) {
714            Ok(into) => into,
715            Err(why) => return Statement::Malformed(why),
716        },
717        _ => return Statement::Malformed("FETCH NEXT ROWSET takes a cursor, FOR n ROWS and INTO".into()),
718    };
719    Statement::FetchRowset { cursor: cursor.into(), rows, into, enabled: false }
720}
721
722/// `FOR n ROWS` at `toks[0]`, and how many tokens it takes. A host variable for n takes no
723/// indicator (Db2 13 SQL, FETCH and INSERT).
724fn rows_clause(toks: &[Tok], pos: Pos) -> Result<(Rows, usize), String> {
725    let (rows, next) = match toks.get(1) {
726        Some(Tok::Number(n)) => (Rows::Constant(n.parse().map_err(|_| format!("FOR {n} ROWS: the number of rows is not an integer"))?), 2),
727        Some(Tok::Host { .. }) => match host_var(toks, 1, pos) {
728            Some((HostVar { indicator: Some(_), .. }, _)) => return Err("FOR n ROWS takes no indicator variable".into()),
729            Some((var, next)) => (Rows::Host(Box::new(var)), next),
730            None => unreachable!("a host token is a host variable"),
731        },
732        _ => return Err("FOR takes a number of rows or a host variable, then ROWS".into()),
733    };
734    match word(toks, next) {
735        "ROWS" => Ok((rows, next + 1)),
736        _ => Err("FOR n takes ROWS".into()),
737    }
738}
739
740/// INSERT, or INSERT ... FOR n ROWS with ATOMIC or NOT ATOMIC CONTINUE ON SQLEXCEPTION, which
741/// IBM writes after VALUES and also before it; without them it is the INSERT of one row.
742fn insert(toks: &[Tok], pos: Pos) -> Statement {
743    let (mut rows, mut atomic, mut kept) = (None, None, Vec::new());
744    let (mut depth, mut i) = (0i32, 0);
745    while i < toks.len() {
746        match &toks[i] {
747            Tok::Punct('(') => depth += 1,
748            Tok::Punct(')') => depth -= 1,
749            _ => {}
750        }
751        if depth == 0 && word(toks, i) == "FOR" && matches!(toks.get(i + 1), Some(Tok::Number(_) | Tok::Host { .. })) {
752            match rows_clause(&toks[i..], pos) {
753                Ok((n, taken)) => {
754                    rows = Some(n);
755                    i += taken;
756                    continue;
757                }
758                Err(why) => return Statement::Malformed(why),
759            }
760        }
761        if depth == 0 && word(toks, i) == "ATOMIC" {
762            atomic = Some(true);
763            i += 1;
764            continue;
765        }
766        if depth == 0 && word(toks, i) == "NOT" && word(toks, i + 1) == "ATOMIC" {
767            if (word(toks, i + 2), word(toks, i + 3), word(toks, i + 4)) != ("CONTINUE", "ON", "SQLEXCEPTION") {
768                return Statement::Malformed("NOT ATOMIC takes CONTINUE ON SQLEXCEPTION".into());
769            }
770            atomic = Some(false);
771            i += 5;
772            continue;
773        }
774        kept.push(toks[i].clone());
775        i += 1;
776    }
777    match rows {
778        None if atomic.is_some() => Statement::Malformed("ATOMIC and NOT ATOMIC take FOR n ROWS".into()),
779        None => change(ChangeKind::Insert, toks, pos),
780        Some(rows) => {
781            let (text, inputs) = render(&kept, pos);
782            Statement::InsertRows { text, inputs, rows, atomic: atomic.unwrap_or(true) }
783        }
784    }
785}
786
787/// CALL of a procedure by its name, with its arguments in parentheses or none.
788fn call(toks: &[Tok], pos: Pos) -> Statement {
789    if matches!(toks.get(1), Some(Tok::Host { .. })) {
790        return Statement::Unsupported("CALL of a procedure a host variable names".into());
791    }
792    let (mut name, mut i) = (Vec::new(), 1);
793    loop {
794        match toks.get(i) {
795            Some(Tok::Word(part) | Tok::Quoted(part)) => name.push(part.clone()),
796            _ => return Statement::Malformed("CALL names no procedure".into()),
797        }
798        i += 1;
799        if toks.get(i) != Some(&Tok::Punct('.')) {
800            break;
801        }
802        i += 1;
803    }
804    let closes_at_end = || {
805        let mut depth = 0i32;
806        for (k, t) in toks[i..].iter().enumerate() {
807            depth += match t {
808                Tok::Punct('(') => 1,
809                Tok::Punct(')') => -1,
810                _ => 0,
811            };
812            if depth == 0 {
813                return i + k == toks.len() - 1;
814            }
815        }
816        false
817    };
818    match (toks.get(i), word(toks, i), word(toks, i + 1)) {
819        (None, _, _) => {}
820        (_, "USING", "DESCRIPTOR") => return Statement::Unsupported("CALL ... USING DESCRIPTOR".into()),
821        (Some(Tok::Punct('(')), _, _) if closes_at_end() => {}
822        _ => return Statement::Malformed("CALL takes a procedure name and its arguments in parentheses".into()),
823    }
824    let (text, args) = render(toks, pos);
825    Statement::Call { procedure: name.join("."), text, args }
826}
827
828fn whenever(toks: &[Tok]) -> Statement {
829    let (condition, at) = match (word(toks, 1), word(toks, 2)) {
830        ("SQLERROR", _) => (Condition::SqlError, 2),
831        ("SQLWARNING", _) => (Condition::SqlWarning, 2),
832        ("NOT", "FOUND") => (Condition::NotFound, 3),
833        _ => return Statement::Malformed("WHENEVER takes SQLERROR, SQLWARNING or NOT FOUND".into()),
834    };
835    let label_at = match (word(toks, at), word(toks, at + 1)) {
836        ("CONTINUE", _) if toks.len() == at + 1 => return Statement::Whenever { condition, action: Action::Continue },
837        ("GO", "TO") => at + 2,
838        ("GOTO", _) => at + 1,
839        _ => return Statement::Malformed("WHENEVER takes CONTINUE or GO TO a label".into()),
840    };
841    let label: String = toks[label_at..]
842        .iter()
843        .map(|t| match t {
844            Tok::Word(s) | Tok::Number(s) => s.clone(),
845            Tok::Host { path, .. } => path.join("."),
846            Tok::Punct(c) => c.to_string(),
847            Tok::Quoted(s) => s.clone(),
848        })
849        .collect();
850    if label.is_empty() {
851        return Statement::Malformed("WHENEVER ... GO TO names no label".into());
852    }
853    Statement::Whenever { condition, action: Action::GoTo(label) }
854}
855
856#[cfg(test)]
857mod tests {
858    use super::*;
859
860    fn st(sql: &str) -> Statement {
861        parse(sql, Pos::default())
862    }
863
864    fn names(vars: &[HostVar]) -> Vec<(&str, Option<&str>)> {
865        vars.iter().map(|h| (h.var.name.as_str(), h.indicator.as_ref().map(|r| r.name.as_str()))).collect()
866    }
867
868    #[test]
869    fn select_into_splits_outputs_from_inputs_and_drops_into() {
870        let Statement::Query { text, inputs, into } = st("SELECT NAME, PHONE INTO :WS-NAME, :WS-PHONE:WS-PHONE-IND FROM CUST WHERE ID = :WS-ID") else { panic!() };
871        assert_eq!(text, "SELECT NAME, PHONE FROM CUST WHERE ID = ?");
872        assert_eq!(names(&into), [("WS-NAME", None), ("WS-PHONE", Some("WS-PHONE-IND"))]);
873        assert_eq!(names(&inputs), [("WS-ID", None)]);
874    }
875
876    #[test]
877    fn indicator_keyword_and_spaced_indicator() {
878        let Statement::Query { into, .. } = st("select a, b into :x indicator :xi, :y :yi from t") else { panic!() };
879        assert_eq!(names(&into), [("X", Some("XI")), ("Y", Some("YI"))]);
880    }
881
882    #[test]
883    fn qualified_and_subscripted_host_variables() {
884        let Statement::Query { into, inputs, .. } = st("SELECT A INTO :CUST.NAME FROM T WHERE K = :TAB(3) AND J = :TAB(IDX)") else { panic!() };
885        assert_eq!((into[0].var.name.as_str(), into[0].var.qualifiers.as_slice()), ("NAME", ["CUST".to_owned()].as_slice()));
886        assert_eq!(inputs[0].var.subscripts, [Expr::Operand(Operand::Literal(Literal::Number("3".into())))]);
887        assert!(matches!(&inputs[1].var.subscripts[0], Expr::Operand(Operand::Ref(r)) if r.name == "IDX"));
888    }
889
890    #[test]
891    fn a_colon_inside_a_string_is_text() {
892        let Statement::Query { text, inputs, .. } = st("SELECT A INTO :X FROM T WHERE B = ':NOT-A-HOST'") else { panic!() };
893        assert_eq!(text, "SELECT A FROM T WHERE B = ':NOT-A-HOST'");
894        assert!(inputs.is_empty());
895    }
896
897    #[test]
898    fn changes_and_positioned_changes() {
899        let Statement::Change { kind, text, inputs, current_of } = st("INSERT INTO T (A, B) VALUES (:A, :B:BI)") else { panic!() };
900        assert_eq!((kind, text.as_str(), current_of), (ChangeKind::Insert, "INSERT INTO T (A, B) VALUES (?, ?)", None));
901        assert_eq!(names(&inputs), [("A", None), ("B", Some("BI"))]);
902        let Statement::Change { current_of, .. } = st("UPDATE EMP SET SAL = :NEW-SAL WHERE CURRENT OF C1") else { panic!() };
903        assert_eq!(current_of.as_deref(), Some("C1"));
904        assert!(matches!(st("DELETE FROM T WHERE K = :K"), Statement::Change { kind: ChangeKind::Delete, current_of: None, .. }));
905    }
906
907    #[test]
908    fn cursors() {
909        let Statement::DeclareCursor(Cursor { name, text, inputs, with_hold, statement, rowset }) = st("DECLARE C1 CURSOR WITH HOLD FOR SELECT NAME FROM EMP WHERE DEPT = :WS-DEPT FOR UPDATE OF SAL") else { panic!() };
910        assert_eq!((name.as_str(), with_hold, statement, rowset), ("C1", true, None, false));
911        assert_eq!(text, "SELECT NAME FROM EMP WHERE DEPT = ? FOR UPDATE OF SAL");
912        assert_eq!(names(&inputs), [("WS-DEPT", None)]);
913        assert_eq!(st("OPEN C1"), Statement::Open { cursor: "C1".into(), declared: None, using: Vec::new(), descriptor: None });
914        assert_eq!(st("CLOSE C1"), Statement::Close { cursor: "C1".into() });
915        let Statement::Fetch { cursor, into } = st("FETCH NEXT FROM C1 INTO :A, :B:BI") else { panic!() };
916        assert_eq!((cursor.as_str(), names(&into).len()), ("C1", 2));
917        assert_eq!(st("FETCH C1"), Statement::Fetch { cursor: "C1".into(), into: Vec::new() });
918    }
919
920    #[test]
921    fn an_into_list_may_omit_colons() {
922        let Statement::Fetch { into, .. } = st("FETCH LON-NHM-ENT-PROJ-WC INTO CSR-ENTITY, CSR-PROJ-ID") else { panic!() };
923        assert_eq!(names(&into), [("CSR-ENTITY", None), ("CSR-PROJ-ID", None)]);
924    }
925
926    #[test]
927    fn hyphenated_cursor_names() {
928        assert_eq!(st("CLOSE PROGRAMS-CSR"), Statement::Close { cursor: "PROGRAMS-CSR".into() });
929        assert!(matches!(st("FETCH FROM PROGRAMS-CSR INTO :A"), Statement::Fetch { cursor, .. } if cursor == "PROGRAMS-CSR"));
930        assert!(matches!(st("DECLARE PROGRAMS-CSR CURSOR FOR SELECT A FROM T"), Statement::DeclareCursor(c) if c.name == "PROGRAMS-CSR"));
931        assert!(matches!(st("DELETE FROM T WHERE CURRENT OF PROGRAMS-CSR"), Statement::Change { current_of: Some(c), .. } if c == "PROGRAMS-CSR"));
932    }
933
934    #[test]
935    fn units_of_work_and_declarations() {
936        assert_eq!(st("COMMIT"), Statement::Commit);
937        assert_eq!(st("COMMIT WORK"), Statement::Commit);
938        assert_eq!(st("ROLLBACK WORK"), Statement::Rollback);
939        assert_eq!(st("ROLLBACK TO SAVEPOINT SP1"), Statement::Unsupported("ROLLBACK TO SAVEPOINT".into()));
940        assert_eq!(st("INCLUDE SQLCA"), Statement::Declaration);
941        assert_eq!(st("BEGIN DECLARE SECTION"), Statement::Declaration);
942        assert_eq!(st("DECLARE EMP TABLE (ID CHAR(6) NOT NULL)"), Statement::Declaration);
943    }
944
945    #[test]
946    fn whenever_actions() {
947        assert_eq!(st("WHENEVER SQLERROR GO TO 9999-ERROR"), Statement::Whenever { condition: Condition::SqlError, action: Action::GoTo("9999-ERROR".into()) });
948        assert_eq!(st("WHENEVER NOT FOUND CONTINUE"), Statement::Whenever { condition: Condition::NotFound, action: Action::Continue });
949        assert_eq!(st("WHENEVER SQLWARNING GOTO :WARN-PARA"), Statement::Whenever { condition: Condition::SqlWarning, action: Action::GoTo("WARN-PARA".into()) });
950    }
951
952    #[test]
953    fn set_and_values_assign_through_a_query() {
954        let Statement::Query { text, into, .. } = st("SET :WS-TS = CURRENT TIMESTAMP") else { panic!() };
955        assert_eq!((text.as_str(), into[0].var.name.as_str()), ("VALUES CURRENT TIMESTAMP", "WS-TS"));
956        let Statement::Query { text, .. } = st("VALUES (:A + 1) INTO :B") else { panic!() };
957        assert_eq!(text, "VALUES (? + 1)");
958    }
959
960    #[test]
961    fn connect_and_set_connection_keep_the_host_variable_naming_the_location() {
962        let target = |sql: &str| match st(sql) {
963            Statement::Connect { what, target } => (what, target.map(|t| t.var.name)),
964            other => panic!("{other:?}"),
965        };
966        assert_eq!(target("CONNECT TO :LOC USER :ID USING :PW"), ("CONNECT".into(), Some("LOC".into())));
967        assert_eq!(target("SET CONNECTION :WS-LOC"), ("SET CONNECTION".into(), Some("WS-LOC".into())));
968        assert_eq!(target("CONNECT RESET"), ("CONNECT".into(), None));
969        assert_eq!(target("CONNECT USER :ID USING :PW"), ("CONNECT".into(), None));
970        assert_eq!(st("SET CURRENT SQLID = 'X'"), Statement::Unsupported("SET".into()));
971    }
972
973    #[test]
974    fn what_is_refused_and_why() {
975        assert_eq!(st("CONNECT TO DB1"), Statement::Connect { what: "CONNECT".into(), target: None });
976        assert!(matches!(st("DISCONNECT ALL"), Statement::Malformed(why) if why.starts_with("DISCONNECT is not a Db2 for z/OS statement")));
977        assert_eq!(st("DESCRIBE INPUT S1 INTO :SQLDA"), Statement::Unsupported("DESCRIBE INPUT".into()));
978        assert_eq!(st("FETCH PRIOR FROM C1 INTO :A"), Statement::Unsupported("a scrollable FETCH".into()));
979        assert_eq!(st("DECLARE C2 SCROLL CURSOR FOR S1"), Statement::DeclareUnsupported { name: "C2".into(), what: "a scrollable cursor".into() });
980        assert!(matches!(st("FETCH INTO :A"), Statement::Malformed(_)));
981        assert!(matches!(st("SELECT A INTO FROM T"), Statement::Malformed(_)));
982        assert!(matches!(st("SELECT A FROM T"), Statement::Malformed(_)));
983        assert!(matches!(st("SELECT A INTO :X FROM T WHERE B = 'OPEN"), Statement::Malformed(_)));
984    }
985
986    #[test]
987    fn two_character_operators_stay_whole() {
988        let Statement::Query { text, .. } = st("SELECT A INTO :X FROM T WHERE B<=:Y AND C <> 1 AND D >= 2 AND E||F = 'A < = B'") else { panic!() };
989        assert_eq!(text, "SELECT A FROM T WHERE B <= ? AND C <> 1 AND D >= 2 AND E || F = 'A < = B'");
990    }
991
992    #[test]
993    fn the_fingerprint_ignores_spelling() {
994        let (Statement::Query { text: a, .. }, Statement::Query { text: b, .. }) = (st("select a into :x from t\n   where k = :k"), st("SELECT A INTO :Y FROM T WHERE K = :J")) else { panic!() };
995        assert_eq!(fingerprint(&a), fingerprint(&b));
996        assert_eq!(fingerprint(""), 0x811c_9dc5);
997    }
998
999    #[test]
1000    fn the_parser_stamps_whenever_in_listing_order_and_numbers_blocks() {
1001        let program = crate::parse(concat!(
1002            "       IDENTIFICATION DIVISION.\n",
1003            "       PROGRAM-ID. T.\n",
1004            "       DATA DIVISION.\n",
1005            "       WORKING-STORAGE SECTION.\n",
1006            "       01 A PIC X(8).\n",
1007            "       PROCEDURE DIVISION.\n",
1008            "           PERFORM LATER.\n",
1009            "           EXEC SQL SELECT X INTO :A FROM T END-EXEC.\n",
1010            "           GOBACK.\n",
1011            "       LATER.\n",
1012            "           EXEC SQL WHENEVER NOT FOUND GO TO DONE END-EXEC.\n",
1013            "           EXEC SQL SELECT Y INTO :A FROM T END-EXEC.\n",
1014            "       DONE.\n",
1015            "           EXIT.\n",
1016        ))
1017        .expect("parses");
1018        let blocks: Vec<&Sql> = sql_blocks(&program);
1019        assert_eq!(blocks.iter().map(|s| s.ordinal).collect::<Vec<_>>(), [1, 2, 3]);
1020        assert_eq!(blocks[0].whenever.not_found, Action::Continue);
1021        assert_eq!(blocks[2].whenever.not_found, Action::GoTo("DONE".into()));
1022    }
1023
1024    #[test]
1025    fn a_cursor_is_read_against_the_declares_before_it() {
1026        let mut cursors = Cursors::default();
1027        assert_eq!(cursors.resolve(st("FETCH C1 INTO :A")), Statement::Malformed("cursor C1 is not declared before this statement".into()));
1028        let Statement::DeclareCursor(c1) = cursors.resolve(st("DECLARE C1 CURSOR WITH HOLD FOR SELECT A FROM T WHERE K = :K")) else { panic!() };
1029        assert_eq!(cursors.resolve(st("OPEN C1")), Statement::Open { cursor: "C1".into(), declared: Some(c1), using: Vec::new(), descriptor: None });
1030        assert!(matches!(cursors.resolve(st("FETCH C1 INTO :A")), Statement::Fetch { .. }));
1031        assert!(matches!(cursors.resolve(st("DELETE FROM T WHERE CURRENT OF C1")), Statement::Change { .. }));
1032        assert!(matches!(cursors.resolve(st("UPDATE T SET A = 1 WHERE CURRENT OF C9")), Statement::Malformed(_)));
1033        let Statement::DeclareCursor(c2) = cursors.resolve(st("DECLARE C2 CURSOR FOR S1")) else { panic!() };
1034        let Statement::Open { declared, using, .. } = cursors.resolve(st("OPEN C2 USING :A, :B")) else { panic!() };
1035        assert_eq!((declared, names(&using)), (Some(c2), vec![("A", None), ("B", None)]));
1036        assert_eq!(cursors.resolve(st("OPEN C1 USING :A")), Statement::Unsupported("OPEN ... USING of a cursor declared for a select-statement".into()));
1037    }
1038
1039    #[test]
1040    fn prepare_and_execute_immediate_take_the_statement_string_from_a_host_variable() {
1041        let Statement::Prepare { name, source, into } = st("PREPARE PRELT98_SQL FROM :SQLSEL-SQL") else { panic!() };
1042        assert_eq!((name.as_str(), source.var.name.as_str(), source.indicator, into), ("PRELT98_SQL", "SQLSEL-SQL", None, None));
1043        let Statement::ExecuteImmediate { source } = st("EXECUTE IMMEDIATE :WS-DYN-SQL") else { panic!() };
1044        assert_eq!(source.var.name, "WS-DYN-SQL");
1045        assert!(matches!(st("PREPARE S1 FROM 'SELECT 1'"), Statement::Malformed(why) if why.contains("PL/I")));
1046        assert!(matches!(st("EXECUTE IMMEDIATE :S :S-IND"), Statement::Malformed(why) if why.contains("no indicator")));
1047        assert!(matches!(st("PREPARE S1 FROM :A :B"), Statement::Malformed(_)));
1048        assert!(matches!(st("PREPARE FROM :A"), Statement::Malformed(_)));
1049        let Statement::Prepare { into: Some((descriptor, names)), .. } = st("PREPARE S1 INTO :SQLDA USING ANY FROM :A") else { panic!() };
1050        assert_eq!((descriptor.var.name.as_str(), names), ("SQLDA", Names::Any));
1051        assert_eq!(st("PREPARE S1 INTO :SQLDA USING BOTH FROM :A"), Statement::Unsupported("PREPARE ... INTO ... USING BOTH".into()));
1052        assert_eq!(st("PREPARE S1 ATTRIBUTES :ATTR FROM :A"), Statement::Unsupported("PREPARE ... ATTRIBUTES".into()));
1053    }
1054
1055    #[test]
1056    fn execute_sends_its_using_list_to_the_parameter_markers() {
1057        assert_eq!(st("EXECUTE MMPREPSTMT"), Statement::Execute { name: "MMPREPSTMT".into(), inputs: Vec::new(), descriptor: None });
1058        let Statement::Execute { name, inputs, .. } = st("EXECUTE INS_STMT USING :EMP-NO, :EMP-NAME:EMP-NAME-IND") else { panic!() };
1059        assert_eq!((name.as_str(), names(&inputs)), ("INS_STMT", vec![("EMP-NO", None), ("EMP-NAME", Some("EMP-NAME-IND"))]));
1060        assert!(matches!(st("EXECUTE S1 USING DESCRIPTOR :SQLDA"), Statement::Execute { descriptor: Some(d), .. } if d.var.name == "SQLDA"));
1061        assert_eq!(st("EXECUTE S1 USING :ARR FOR 10 ROWS"), Statement::Unsupported("a multi-row EXECUTE".into()));
1062        assert!(matches!(st("EXECUTE S1 USING :A,"), Statement::Malformed(_)));
1063        assert!(matches!(st("EXECUTE S1 USING A"), Statement::Malformed(_)));
1064    }
1065
1066    #[test]
1067    fn describe_and_fetch_using_descriptor_name_the_sqlda() {
1068        assert_eq!(st("DESCRIBE OUTPUT S1 INTO :SDSC"), Statement::Describe { name: "S1".into(), descriptor: HostVar { var: reference(&["SDSC".into()], &[], Pos::default()), indicator: None, colon: true }, names: Names::Names });
1069        assert!(matches!(st("DESCRIBE S1 INTO :D USING LABELS"), Statement::Describe { names: Names::Labels, .. }));
1070        assert_eq!(st("DESCRIBE S1 INTO :D USING BOTH"), Statement::Unsupported("DESCRIBE ... USING BOTH".into()));
1071        assert_eq!(st("DESCRIBE CURSOR C1 INTO :D"), Statement::Unsupported("DESCRIBE CURSOR".into()));
1072        assert!(matches!(st("DESCRIBE S1 :D"), Statement::Malformed(_)));
1073        assert!(matches!(st("DESCRIBE S1 INTO :D :I"), Statement::Malformed(_)));
1074        let Statement::FetchDescriptor { cursor, descriptor } = st("FETCH DT USING DESCRIPTOR :SQLDA") else { panic!() };
1075        assert_eq!((cursor.as_str(), descriptor.var.name.as_str()), ("DT", "SQLDA"));
1076        let mut cursors = Cursors::default();
1077        cursors.resolve(st("DECLARE C1 CURSOR FOR SELECT A FROM T"));
1078        assert_eq!(cursors.resolve(st("OPEN C1 USING DESCRIPTOR :D")), Statement::Unsupported("OPEN ... USING of a cursor declared for a select-statement".into()));
1079        assert!(matches!(cursors.resolve(st("FETCH C9 USING DESCRIPTOR :D")), Statement::Malformed(_)));
1080    }
1081
1082    #[test]
1083    fn a_cursor_for_a_prepared_statement_names_it() {
1084        let Statement::DeclareCursor(c) = st("DECLARE DT CURSOR WITH HOLD FOR DYN-STMT") else { panic!() };
1085        assert_eq!((c.name.as_str(), c.statement.as_deref(), c.with_hold, c.text.as_str()), ("DT", Some("DYN-STMT"), true, ""));
1086        assert!(matches!(st("DECLARE C1 CURSOR FOR S1 S2"), Statement::Malformed(_)));
1087        assert!(matches!(st("OPEN C1 USING DESCRIPTOR :SQLDA"), Statement::Open { descriptor: Some(d), .. } if d.var.name == "SQLDA"));
1088        assert!(matches!(st("OPEN C1 FOR"), Statement::Malformed(_)));
1089    }
1090
1091    #[test]
1092    fn a_rowset_fetch_takes_next_rowset_its_rows_and_arrays_from_a_rowset_cursor() {
1093        let mut cursors = Cursors::default();
1094        cursors.resolve(st("DECLARE C1 CURSOR WITH HOLD WITH ROWSET POSITIONING FOR SELECT A, B FROM T"));
1095        cursors.resolve(st("DECLARE C2 CURSOR FOR SELECT A FROM T"));
1096        let Statement::FetchRowset { cursor, rows, into, enabled } = cursors.resolve(st("FETCH NEXT ROWSET FROM C1 FOR 5 ROWS INTO :COL1 :COL1IND, :COL2")) else { panic!() };
1097        assert_eq!((cursor.as_str(), rows, names(&into), enabled), ("C1", Rows::Constant(5), vec![("COL1", Some("COL1IND")), ("COL2", None)], true));
1098        let Statement::FetchRowset { rows: Rows::Host(n), enabled, .. } = cursors.resolve(st("FETCH NEXT ROWSET C2 FOR :N ROWS INTO :A")) else { panic!() };
1099        assert_eq!((n.var.name.as_str(), enabled), ("N", false));
1100        assert!(matches!(st("FETCH NEXT ROWSET FROM C1"), Statement::FetchRowset { rows: Rows::Implicit, into, .. } if into.is_empty()));
1101        assert!(matches!(st("FETCH NEXT ROWSET FROM C1 FOR :N :NI ROWS INTO :A"), Statement::Malformed(why) if why.contains("indicator")));
1102        assert!(matches!(st("FETCH NEXT FROM BINCSR FOR 50 ROWS INTO :WS-BIN-TABLE"), Statement::Malformed(why) if why.contains("NEXT ROWSET")));
1103        assert_eq!(st("FETCH NEXT ROWSET FROM C1 FOR 5 ROWS INTO DESCRIPTOR :D"), Statement::Unsupported("FETCH ... INTO DESCRIPTOR".into()));
1104        for scrolled in ["FETCH PRIOR ROWSET FROM C1 FOR 5 ROWS INTO :A", "FETCH ROWSET STARTING AT ABSOLUTE 3 FROM C1 FOR 2 ROWS INTO :A", "FETCH FIRST ROWSET FROM C1 INTO :A"] {
1105            assert_eq!(st(scrolled), Statement::Unsupported("a scrollable FETCH".into()), "{scrolled}");
1106        }
1107        assert!(matches!(Cursors::default().resolve(st("FETCH NEXT ROWSET FROM C9 INTO :A")), Statement::Malformed(_)));
1108    }
1109
1110    #[test]
1111    fn a_multiple_row_insert_keeps_the_insert_of_one_row_and_its_atomicity() {
1112        let Statement::InsertRows { text, inputs, rows, atomic } = st("INSERT INTO DSN8D10.ACT (ACTNO, ACTKWD, ACTDESC) VALUES (:HVA1, :HVA2 :HVA2-IND, 'X') FOR :NUM-ROWS ROWS") else { panic!() };
1113        assert_eq!(text, "INSERT INTO DSN8D10.ACT (ACTNO, ACTKWD, ACTDESC) VALUES (?, ?, 'X')");
1114        assert_eq!((names(&inputs), atomic), (vec![("HVA1", None), ("HVA2", Some("HVA2-IND"))], true));
1115        assert!(matches!(rows, Rows::Host(h) if h.var.name == "NUM-ROWS" && h.indicator.is_none()));
1116        let Statement::InsertRows { text, rows, atomic, .. } = st("INSERT INTO T1 FOR 5 ROWS VALUES (:HVA) NOT ATOMIC CONTINUE ON SQLEXCEPTION") else { panic!() };
1117        assert_eq!((text.as_str(), rows, atomic), ("INSERT INTO T1 VALUES (?)", Rows::Constant(5), false));
1118        assert!(matches!(st("INSERT INTO T VALUES (:A) FOR 3 ROWS ATOMIC"), Statement::InsertRows { atomic: true, .. }));
1119        assert!(matches!(st("INSERT INTO T VALUES (:A) FOR 3 ROWS NOT ATOMIC"), Statement::Malformed(_)));
1120        assert!(matches!(st("INSERT INTO T VALUES (:A) ATOMIC"), Statement::Malformed(_)));
1121        assert!(matches!(st("INSERT INTO T VALUES (:A)"), Statement::Change { kind: ChangeKind::Insert, .. }));
1122        let row_of_rowset = Statement::Unsupported("a positioned UPDATE or DELETE FOR ROW n OF ROWSET".into());
1123        assert_eq!(st("UPDATE T SET A = 1 WHERE CURRENT OF C1 FOR ROW :N OF ROWSET"), row_of_rowset);
1124        assert_eq!(st("DELETE FROM T WHERE CURRENT OF C1 FOR ROW 5 OF ROWSET"), row_of_rowset);
1125    }
1126
1127    #[test]
1128    fn call_names_its_procedure_and_sends_its_arguments() {
1129        let Statement::Call { procedure, text, args } = st("CALL PDAPROD.PDASP2 (:PDASP2-USERID, :PDASP2-STATUS :STATUS-IND, 'Y', NULL)") else { panic!() };
1130        assert_eq!((procedure.as_str(), text.as_str()), ("PDAPROD.PDASP2", "CALL PDAPROD.PDASP2 (?, ?, 'Y', NULL)"));
1131        assert_eq!(names(&args), [("PDASP2-USERID", None), ("PDASP2-STATUS", Some("STATUS-IND"))]);
1132        assert!(matches!(st("CALL PCTPROC"), Statement::Call { args, .. } if args.is_empty()));
1133        assert!(matches!(st("CALL P()"), Statement::Call { args, .. } if args.is_empty()));
1134        assert_eq!(st("CALL :PROC-NAME (:A)"), Statement::Unsupported("CALL of a procedure a host variable names".into()));
1135        assert_eq!(st("CALL P USING DESCRIPTOR :D"), Statement::Unsupported("CALL ... USING DESCRIPTOR".into()));
1136        for broken in ["CALL", "CALL P (:A", "CALL P (:A) (:B)", "CALL P :A"] {
1137            assert!(matches!(st(broken), Statement::Malformed(_)), "{broken}");
1138        }
1139    }
1140
1141    fn sql_blocks(program: &crate::ast::Program) -> Vec<&Sql> {
1142        program
1143            .paragraphs
1144            .iter()
1145            .flat_map(|p| &p.statements)
1146            .filter_map(|s| match s {
1147                crate::ast::Stmt::Exec(block) => block.sql.as_ref(),
1148                _ => None,
1149            })
1150            .collect()
1151    }
1152}