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