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}
15
16#[derive(Clone, Debug, Default, PartialEq, Eq)]
17pub enum Action {
18    #[default]
19    Continue,
20    GoTo(String),
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum Condition {
25    SqlError,
26    NotFound,
27    SqlWarning,
28}
29
30/// The WHENEVER actions in force at a statement.
31#[derive(Clone, Debug, Default, PartialEq, Eq)]
32pub struct Whenever {
33    pub sqlerror: Action,
34    pub not_found: Action,
35    pub sqlwarning: Action,
36}
37
38impl Whenever {
39    pub fn set(&mut self, condition: Condition, action: Action) {
40        match condition {
41            Condition::SqlError => self.sqlerror = action,
42            Condition::NotFound => self.not_found = action,
43            Condition::SqlWarning => self.sqlwarning = action,
44        }
45    }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum ChangeKind {
50    Insert,
51    Update,
52    Delete,
53}
54
55/// A cursor as its DECLARE gives it: the query OPEN runs and the host variables OPEN sends.
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct Cursor {
58    pub name: String,
59    pub text: String,
60    pub inputs: Vec<HostVar>,
61    pub with_hold: bool,
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum Statement {
66    /// SELECT ... INTO, VALUES ... INTO, and SET :host-variable = expression.
67    Query { text: String, inputs: Vec<HostVar>, into: Vec<HostVar> },
68    /// INSERT, UPDATE and DELETE; `current_of` names the cursor of a positioned UPDATE or DELETE.
69    Change { kind: ChangeKind, text: String, inputs: Vec<HostVar>, current_of: Option<String> },
70    DeclareCursor(Cursor),
71    /// A cursor ironwork does not run, such as one for a prepared statement, and what it is.
72    DeclareUnsupported { name: String, what: String },
73    /// `declared` is the cursor's DECLARE, which [`Cursors::resolve`] fills.
74    Open { cursor: String, declared: Option<Cursor> },
75    Fetch { cursor: String, into: Vec<HostVar> },
76    Close { cursor: String },
77    Commit,
78    Rollback,
79    Whenever { condition: Condition, action: Action },
80    /// INCLUDE, DECLARE SECTION, DECLARE TABLE and DECLARE STATEMENT, which declare and do nothing.
81    Declaration,
82    /// A statement ironwork does not run, named by what it is.
83    Unsupported(String),
84    /// A statement the precompiler refuses, with the reason: it does not read as its verb requires,
85    /// or it is not Db2 for z/OS.
86    Malformed(String),
87}
88
89impl Statement {
90    /// Every host variable and indicator the statement names.
91    pub fn references(&self) -> Vec<&Ref> {
92        let vars: Vec<&HostVar> = match self {
93            Statement::Query { inputs, into, .. } => into.iter().chain(inputs).collect(),
94            Statement::Change { inputs, .. } | Statement::DeclareCursor(Cursor { inputs, .. }) => inputs.iter().collect(),
95            Statement::Fetch { into, .. } => into.iter().collect(),
96            _ => Vec::new(),
97        };
98        vars.into_iter().flat_map(|h| std::iter::once(&h.var).chain(&h.indicator)).collect()
99    }
100}
101
102/// The cursors declared so far in a listing. The precompiler reads a cursor's name against the
103/// DECLAREs before it, so a statement naming a cursor declared later, or never, is refused.
104#[derive(Debug, Default)]
105pub struct Cursors(HashMap<String, Result<Cursor, String>>);
106
107impl Cursors {
108    /// Records a DECLARE CURSOR, gives OPEN its cursor, and refuses a cursor not yet declared.
109    pub fn resolve(&mut self, statement: Statement) -> Statement {
110        let named = match &statement {
111            Statement::DeclareCursor(c) => {
112                self.0.insert(c.name.clone(), Ok(c.clone()));
113                return statement;
114            }
115            Statement::DeclareUnsupported { name, what } => {
116                self.0.insert(name.clone(), Err(what.clone()));
117                return statement;
118            }
119            Statement::Open { cursor, .. } | Statement::Fetch { cursor, .. } | Statement::Close { cursor, .. } | Statement::Change { current_of: Some(cursor), .. } => cursor.clone(),
120            _ => return statement,
121        };
122        match (self.0.get(&named), statement) {
123            (None, _) => Statement::Malformed(format!("cursor {named} is not declared before this statement")),
124            (Some(Err(what)), _) => Statement::Unsupported(what.clone()),
125            (Some(Ok(c)), Statement::Open { cursor, .. }) => Statement::Open { cursor, declared: Some(c.clone()) },
126            (Some(Ok(_)), statement) => statement,
127        }
128    }
129}
130
131/// An EXEC SQL block's typed statement, its place among the program's EXEC SQL blocks in listing
132/// order (from 1), and the WHENEVER actions in force where it stands.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct Sql {
135    pub statement: Statement,
136    pub ordinal: u32,
137    pub whenever: Whenever,
138}
139
140pub use rt::sql::fingerprint;
141
142/// The statement in `body`, the text between EXEC SQL and END-EXEC.
143pub fn parse(body: &str, pos: Pos) -> Statement {
144    match lex(body) {
145        Ok(toks) => statement(&toks, pos),
146        Err(why) => Statement::Malformed(why),
147    }
148}
149
150#[derive(Clone, Debug, PartialEq, Eq)]
151enum Tok {
152    Word(String),
153    Quoted(String),
154    Number(String),
155    Punct(char),
156    Host { path: Vec<String>, subscripts: Vec<String> },
157}
158
159fn lex(sql: &str) -> Result<Vec<Tok>, String> {
160    let chars: Vec<char> = sql.chars().collect();
161    let (mut out, mut i) = (Vec::new(), 0);
162    let text = |from: usize, to: usize| chars[from..to].iter().collect::<String>();
163    while let Some(&c) = chars.get(i) {
164        if c.is_whitespace() {
165            i += 1;
166        } else if c == '\'' || c == '"' {
167            let start = i;
168            i += 1;
169            loop {
170                match chars.get(i) {
171                    None => return Err("a quoted string is not closed".into()),
172                    Some(&q) if q == c && chars.get(i + 1) == Some(&c) => i += 2,
173                    Some(&q) if q == c => {
174                        i += 1;
175                        break;
176                    }
177                    Some(_) => i += 1,
178                }
179            }
180            out.push(Tok::Quoted(text(start, i)));
181        } else if c == ':' && chars.get(i + 1).is_some_and(char::is_ascii_alphanumeric) {
182            i += 1;
183            let mut path = Vec::new();
184            loop {
185                let start = i;
186                while chars.get(i).is_some_and(|&n| n.is_ascii_alphanumeric() || n == '-' || n == '_') {
187                    i += 1;
188                }
189                path.push(text(start, i).to_ascii_uppercase());
190                if chars.get(i) == Some(&'.') && chars.get(i + 1).is_some_and(char::is_ascii_alphanumeric) {
191                    i += 1;
192                } else {
193                    break;
194                }
195            }
196            let mut subscripts = Vec::new();
197            if chars.get(i) == Some(&'(') {
198                let close = chars[i..].iter().position(|&n| n == ')').ok_or("a subscript is not closed")? + i;
199                subscripts = text(i + 1, close).split(',').map(|s| s.trim().to_ascii_uppercase()).collect();
200                if subscripts.iter().any(String::is_empty) {
201                    return Err("a subscript is empty".into());
202                }
203                i = close + 1;
204            }
205            out.push(Tok::Host { path, subscripts });
206        } else if c.is_ascii_alphabetic() || matches!(c, '_' | '#' | '@' | '$') {
207            // Cursor and statement names in COBOL programs take hyphens, as in PROGRAMS-CSR.
208            let start = i;
209            while chars.get(i).is_some_and(|&n| {
210                n.is_ascii_alphanumeric() || matches!(n, '_' | '#' | '@' | '$') || (n == '-' && chars.get(i + 1).is_some_and(char::is_ascii_alphanumeric))
211            }) {
212                i += 1;
213            }
214            out.push(Tok::Word(text(start, i).to_ascii_uppercase()));
215        } else if c.is_ascii_digit() {
216            let start = i;
217            while chars.get(i).is_some_and(|&n| n.is_ascii_digit() || n == '.') {
218                i += 1;
219            }
220            out.push(Tok::Number(text(start, i)));
221        } else {
222            out.push(Tok::Punct(c));
223            i += 1;
224        }
225    }
226    Ok(out)
227}
228
229fn word(toks: &[Tok], i: usize) -> &str {
230    match toks.get(i) {
231        Some(Tok::Word(w)) => w,
232        _ => "",
233    }
234}
235
236fn reference(path: &[String], subscripts: &[String], pos: Pos) -> Ref {
237    let mut qualifiers = path.to_vec();
238    let name = qualifiers.pop().unwrap_or_default();
239    qualifiers.reverse();
240    let subscripts = subscripts
241        .iter()
242        .map(|s| {
243            Expr::Operand(if s.chars().all(|c| c.is_ascii_digit()) {
244                Operand::Literal(Literal::Number(s.clone()))
245            } else {
246                Operand::Ref(Ref { name: s.clone(), qualifiers: Vec::new(), subscripts: Vec::new(), refmod: None, pos })
247            })
248        })
249        .collect();
250    Ref { name, qualifiers, subscripts, refmod: None, pos }
251}
252
253/// The host variable starting at `toks[i]`, with the indicator after it, and where it ends.
254fn host_var(toks: &[Tok], i: usize, pos: Pos) -> Option<(HostVar, usize)> {
255    let Some(Tok::Host { path, subscripts }) = toks.get(i) else { return None };
256    let var = reference(path, subscripts, pos);
257    let at = if word(toks, i + 1) == "INDICATOR" { i + 2 } else { i + 1 };
258    match toks.get(at) {
259        Some(Tok::Host { path, subscripts }) => Some((HostVar { var, indicator: Some(reference(path, subscripts, pos)) }, at + 1)),
260        _ => Some((HostVar { var, indicator: None }, i + 1)),
261    }
262}
263
264/// The canonical text of `toks`, each host variable replaced by `?`, and those host variables.
265fn render(toks: &[Tok], pos: Pos) -> (String, Vec<HostVar>) {
266    let (mut pieces, mut vars, mut i) = (Vec::<String>::new(), Vec::new(), 0);
267    while i < toks.len() {
268        if let Some((var, next)) = host_var(toks, i, pos) {
269            vars.push(var);
270            pieces.push("?".into());
271            i = next;
272            continue;
273        }
274        pieces.push(match &toks[i] {
275            Tok::Word(s) | Tok::Quoted(s) | Tok::Number(s) => s.clone(),
276            Tok::Punct(c) => c.to_string(),
277            Tok::Host { .. } => unreachable!("host_var takes every host token"),
278        });
279        i += 1;
280    }
281    let mut text = String::new();
282    for (n, piece) in pieces.iter().enumerate() {
283        let operator = n > 0 && matches!((pieces[n - 1].as_str(), piece.as_str()), ("<" | ">" | "!" | "¬" | "^", "=") | ("<", ">") | ("|", "|"));
284        let glued = n == 0 || operator || matches!(piece.as_str(), ")" | "," | ".") || matches!(pieces[n - 1].as_str(), "(" | ".");
285        if !glued {
286            text.push(' ');
287        }
288        text.push_str(piece);
289    }
290    (text, vars)
291}
292
293/// A comma-separated list of host variables, as INTO and FETCH ... INTO take.
294fn host_list(toks: &[Tok], pos: Pos) -> Result<Vec<HostVar>, String> {
295    let (mut out, mut i) = (Vec::new(), 0);
296    while i < toks.len() {
297        // Only host variables stand in an INTO list, so a name written without its colon is one
298        // (assumption S7).
299        let (var, next) = match toks.get(i) {
300            Some(Tok::Word(name)) => (HostVar { var: reference(std::slice::from_ref(name), &[], pos), indicator: None }, i + 1),
301            _ => host_var(toks, i, pos).ok_or("INTO lists something other than host variables")?,
302        };
303        out.push(var);
304        i = next;
305        match toks.get(i) {
306            None => {}
307            Some(Tok::Punct(',')) => i += 1,
308            Some(_) => return Err("INTO lists something other than host variables".into()),
309        }
310    }
311    if out.is_empty() {
312        return Err("INTO names no host variable".into());
313    }
314    Ok(out)
315}
316
317/// Where `target` first stands outside parentheses, from `from`.
318fn top_level(toks: &[Tok], from: usize, target: &str) -> Option<usize> {
319    let mut depth = 0i32;
320    for (i, t) in toks.iter().enumerate().skip(from) {
321        match t {
322            Tok::Punct('(') => depth += 1,
323            Tok::Punct(')') => depth -= 1,
324            Tok::Word(w) if depth == 0 && w == target => return Some(i),
325            _ => {}
326        }
327    }
328    None
329}
330
331fn statement(toks: &[Tok], pos: Pos) -> Statement {
332    let verb = word(toks, 0);
333    match verb {
334        "SELECT" | "VALUES" => into_query(toks, pos),
335        "SET" if matches!(toks.get(1), Some(Tok::Host { .. })) => set_host(toks, pos),
336        "INSERT" => change(ChangeKind::Insert, toks, pos),
337        "UPDATE" => change(ChangeKind::Update, toks, pos),
338        "DELETE" => change(ChangeKind::Delete, toks, pos),
339        "DECLARE" => declare(toks, pos),
340        "OPEN" => match (word(toks, 1), toks.len()) {
341            ("", _) => Statement::Malformed("OPEN names no cursor".into()),
342            (cursor, 2) => Statement::Open { cursor: cursor.into(), declared: None },
343            _ => Statement::Unsupported("OPEN with USING".into()),
344        },
345        "FETCH" => fetch(toks, pos),
346        "CLOSE" => match (word(toks, 1), toks.len()) {
347            ("", _) => Statement::Malformed("CLOSE names no cursor".into()),
348            (cursor, 2) => Statement::Close { cursor: cursor.into() },
349            _ => Statement::Malformed("CLOSE takes only a cursor name".into()),
350        },
351        "COMMIT" if toks.len() == 1 || (toks.len() == 2 && word(toks, 1) == "WORK") => Statement::Commit,
352        "ROLLBACK" if toks.len() == 1 || (toks.len() == 2 && word(toks, 1) == "WORK") => Statement::Rollback,
353        "ROLLBACK" => Statement::Unsupported("ROLLBACK TO SAVEPOINT".into()),
354        "WHENEVER" => whenever(toks),
355        "INCLUDE" => Statement::Declaration,
356        "BEGIN" | "END" if word(toks, 1) == "DECLARE" => Statement::Declaration,
357        "DISCONNECT" => Statement::Malformed("DISCONNECT is not a Db2 for z/OS statement; Db2 ends a connection with RELEASE and a commit".into()),
358        "" => Statement::Malformed("the block holds no statement".into()),
359        other => Statement::Unsupported(other.into()),
360    }
361}
362
363fn into_query(toks: &[Tok], pos: Pos) -> Statement {
364    let Some(into) = top_level(toks, 1, "INTO") else {
365        return Statement::Malformed(format!("{} has no INTO", word(toks, 0)));
366    };
367    let end = top_level(toks, into + 1, "FROM").unwrap_or(toks.len());
368    let into_vars = match host_list(&toks[into + 1..end], pos) {
369        Ok(v) => v,
370        Err(why) => return Statement::Malformed(why),
371    };
372    let rest: Vec<Tok> = toks[..into].iter().chain(&toks[end..]).cloned().collect();
373    let (text, inputs) = render(&rest, pos);
374    Statement::Query { text, inputs, into: into_vars }
375}
376
377fn set_host(toks: &[Tok], pos: Pos) -> Statement {
378    let Some((target, next)) = host_var(toks, 1, pos) else { unreachable!("statement checks the host variable") };
379    if toks.get(next) != Some(&Tok::Punct('=')) || next + 1 >= toks.len() {
380        return Statement::Malformed("SET :host-variable takes = and an expression".into());
381    }
382    let expression: Vec<Tok> = std::iter::once(Tok::Word("VALUES".into())).chain(toks[next + 1..].iter().cloned()).collect();
383    let (text, inputs) = render(&expression, pos);
384    Statement::Query { text, inputs, into: vec![target] }
385}
386
387fn change(kind: ChangeKind, toks: &[Tok], pos: Pos) -> Statement {
388    let n = toks.len();
389    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())
390        .then(|| word(toks, n - 1).to_owned());
391    let (text, inputs) = render(toks, pos);
392    Statement::Change { kind, text, inputs, current_of }
393}
394
395fn declare(toks: &[Tok], pos: Pos) -> Statement {
396    let name = word(toks, 1);
397    match word(toks, 2) {
398        "TABLE" | "STATEMENT" => return Statement::Declaration,
399        _ if name.is_empty() => return Statement::Malformed("DECLARE names nothing".into()),
400        _ => {}
401    }
402    let Some(cursor) = (2..toks.len()).find(|&i| word(toks, i) == "CURSOR") else {
403        return Statement::Unsupported(format!("DECLARE {}", word(toks, 2)));
404    };
405    let unsupported = |what: &str| Statement::DeclareUnsupported { name: name.into(), what: what.into() };
406    if (2..cursor).any(|i| word(toks, i) == "SCROLL" && word(toks, i - 1) != "NO") {
407        return unsupported("a scrollable cursor");
408    }
409    let Some(for_at) = top_level(toks, cursor + 1, "FOR") else {
410        return Statement::Malformed("DECLARE CURSOR has no FOR".into());
411    };
412    let with_hold = (cursor + 1..for_at).any(|i| word(toks, i) == "HOLD" && word(toks, i - 1) == "WITH");
413    let query = &toks[for_at + 1..];
414    if !matches!(word(query, 0), "SELECT" | "WITH" | "VALUES") && !matches!(query.first(), Some(Tok::Punct('('))) {
415        return unsupported("a cursor for a prepared statement");
416    }
417    let (text, inputs) = render(query, pos);
418    Statement::DeclareCursor(Cursor { name: name.into(), text, inputs, with_hold })
419}
420
421fn fetch(toks: &[Tok], pos: Pos) -> Statement {
422    let mut i = 1;
423    if word(toks, i) == "NEXT" {
424        i += 1;
425    }
426    if matches!(word(toks, i), "PRIOR" | "FIRST" | "LAST" | "ABSOLUTE" | "RELATIVE" | "BEFORE" | "AFTER" | "CURRENT" | "SENSITIVE" | "INSENSITIVE") {
427        return Statement::Unsupported("a scrollable FETCH".into());
428    }
429    if word(toks, i) == "FROM" {
430        i += 1;
431    }
432    let cursor = word(toks, i);
433    if cursor.is_empty() {
434        return Statement::Malformed("FETCH names no cursor".into());
435    }
436    i += 1;
437    match word(toks, i) {
438        "" if i == toks.len() => Statement::Fetch { cursor: cursor.into(), into: Vec::new() },
439        "INTO" => match host_list(&toks[i + 1..], pos) {
440            Ok(into) => Statement::Fetch { cursor: cursor.into(), into },
441            Err(why) => Statement::Malformed(why),
442        },
443        "FOR" => Statement::Unsupported("a multi-row FETCH".into()),
444        "USING" => Statement::Unsupported("FETCH USING DESCRIPTOR".into()),
445        _ => Statement::Malformed("FETCH takes a cursor and INTO".into()),
446    }
447}
448
449fn whenever(toks: &[Tok]) -> Statement {
450    let (condition, at) = match (word(toks, 1), word(toks, 2)) {
451        ("SQLERROR", _) => (Condition::SqlError, 2),
452        ("SQLWARNING", _) => (Condition::SqlWarning, 2),
453        ("NOT", "FOUND") => (Condition::NotFound, 3),
454        _ => return Statement::Malformed("WHENEVER takes SQLERROR, SQLWARNING or NOT FOUND".into()),
455    };
456    let label_at = match (word(toks, at), word(toks, at + 1)) {
457        ("CONTINUE", _) if toks.len() == at + 1 => return Statement::Whenever { condition, action: Action::Continue },
458        ("GO", "TO") => at + 2,
459        ("GOTO", _) => at + 1,
460        _ => return Statement::Malformed("WHENEVER takes CONTINUE or GO TO a label".into()),
461    };
462    let label: String = toks[label_at..]
463        .iter()
464        .map(|t| match t {
465            Tok::Word(s) | Tok::Number(s) => s.clone(),
466            Tok::Host { path, .. } => path.join("."),
467            Tok::Punct(c) => c.to_string(),
468            Tok::Quoted(s) => s.clone(),
469        })
470        .collect();
471    if label.is_empty() {
472        return Statement::Malformed("WHENEVER ... GO TO names no label".into());
473    }
474    Statement::Whenever { condition, action: Action::GoTo(label) }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    fn st(sql: &str) -> Statement {
482        parse(sql, Pos::default())
483    }
484
485    fn names(vars: &[HostVar]) -> Vec<(&str, Option<&str>)> {
486        vars.iter().map(|h| (h.var.name.as_str(), h.indicator.as_ref().map(|r| r.name.as_str()))).collect()
487    }
488
489    #[test]
490    fn select_into_splits_outputs_from_inputs_and_drops_into() {
491        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!() };
492        assert_eq!(text, "SELECT NAME, PHONE FROM CUST WHERE ID = ?");
493        assert_eq!(names(&into), [("WS-NAME", None), ("WS-PHONE", Some("WS-PHONE-IND"))]);
494        assert_eq!(names(&inputs), [("WS-ID", None)]);
495    }
496
497    #[test]
498    fn indicator_keyword_and_spaced_indicator() {
499        let Statement::Query { into, .. } = st("select a, b into :x indicator :xi, :y :yi from t") else { panic!() };
500        assert_eq!(names(&into), [("X", Some("XI")), ("Y", Some("YI"))]);
501    }
502
503    #[test]
504    fn qualified_and_subscripted_host_variables() {
505        let Statement::Query { into, inputs, .. } = st("SELECT A INTO :CUST.NAME FROM T WHERE K = :TAB(3) AND J = :TAB(IDX)") else { panic!() };
506        assert_eq!((into[0].var.name.as_str(), into[0].var.qualifiers.as_slice()), ("NAME", ["CUST".to_owned()].as_slice()));
507        assert_eq!(inputs[0].var.subscripts, [Expr::Operand(Operand::Literal(Literal::Number("3".into())))]);
508        assert!(matches!(&inputs[1].var.subscripts[0], Expr::Operand(Operand::Ref(r)) if r.name == "IDX"));
509    }
510
511    #[test]
512    fn a_colon_inside_a_string_is_text() {
513        let Statement::Query { text, inputs, .. } = st("SELECT A INTO :X FROM T WHERE B = ':NOT-A-HOST'") else { panic!() };
514        assert_eq!(text, "SELECT A FROM T WHERE B = ':NOT-A-HOST'");
515        assert!(inputs.is_empty());
516    }
517
518    #[test]
519    fn changes_and_positioned_changes() {
520        let Statement::Change { kind, text, inputs, current_of } = st("INSERT INTO T (A, B) VALUES (:A, :B:BI)") else { panic!() };
521        assert_eq!((kind, text.as_str(), current_of), (ChangeKind::Insert, "INSERT INTO T (A, B) VALUES (?, ?)", None));
522        assert_eq!(names(&inputs), [("A", None), ("B", Some("BI"))]);
523        let Statement::Change { current_of, .. } = st("UPDATE EMP SET SAL = :NEW-SAL WHERE CURRENT OF C1") else { panic!() };
524        assert_eq!(current_of.as_deref(), Some("C1"));
525        assert!(matches!(st("DELETE FROM T WHERE K = :K"), Statement::Change { kind: ChangeKind::Delete, current_of: None, .. }));
526    }
527
528    #[test]
529    fn cursors() {
530        let Statement::DeclareCursor(Cursor { name, text, inputs, with_hold }) = st("DECLARE C1 CURSOR WITH HOLD FOR SELECT NAME FROM EMP WHERE DEPT = :WS-DEPT FOR UPDATE OF SAL") else { panic!() };
531        assert_eq!((name.as_str(), with_hold), ("C1", true));
532        assert_eq!(text, "SELECT NAME FROM EMP WHERE DEPT = ? FOR UPDATE OF SAL");
533        assert_eq!(names(&inputs), [("WS-DEPT", None)]);
534        assert_eq!(st("OPEN C1"), Statement::Open { cursor: "C1".into(), declared: None });
535        assert_eq!(st("CLOSE C1"), Statement::Close { cursor: "C1".into() });
536        let Statement::Fetch { cursor, into } = st("FETCH NEXT FROM C1 INTO :A, :B:BI") else { panic!() };
537        assert_eq!((cursor.as_str(), names(&into).len()), ("C1", 2));
538        assert_eq!(st("FETCH C1"), Statement::Fetch { cursor: "C1".into(), into: Vec::new() });
539    }
540
541    #[test]
542    fn an_into_list_may_omit_colons() {
543        let Statement::Fetch { into, .. } = st("FETCH LON-NHM-ENT-PROJ-WC INTO CSR-ENTITY, CSR-PROJ-ID") else { panic!() };
544        assert_eq!(names(&into), [("CSR-ENTITY", None), ("CSR-PROJ-ID", None)]);
545    }
546
547    #[test]
548    fn hyphenated_cursor_names() {
549        assert_eq!(st("CLOSE PROGRAMS-CSR"), Statement::Close { cursor: "PROGRAMS-CSR".into() });
550        assert!(matches!(st("FETCH FROM PROGRAMS-CSR INTO :A"), Statement::Fetch { cursor, .. } if cursor == "PROGRAMS-CSR"));
551        assert!(matches!(st("DECLARE PROGRAMS-CSR CURSOR FOR SELECT A FROM T"), Statement::DeclareCursor(c) if c.name == "PROGRAMS-CSR"));
552        assert!(matches!(st("DELETE FROM T WHERE CURRENT OF PROGRAMS-CSR"), Statement::Change { current_of: Some(c), .. } if c == "PROGRAMS-CSR"));
553    }
554
555    #[test]
556    fn units_of_work_and_declarations() {
557        assert_eq!(st("COMMIT"), Statement::Commit);
558        assert_eq!(st("COMMIT WORK"), Statement::Commit);
559        assert_eq!(st("ROLLBACK WORK"), Statement::Rollback);
560        assert_eq!(st("ROLLBACK TO SAVEPOINT SP1"), Statement::Unsupported("ROLLBACK TO SAVEPOINT".into()));
561        assert_eq!(st("INCLUDE SQLCA"), Statement::Declaration);
562        assert_eq!(st("BEGIN DECLARE SECTION"), Statement::Declaration);
563        assert_eq!(st("DECLARE EMP TABLE (ID CHAR(6) NOT NULL)"), Statement::Declaration);
564    }
565
566    #[test]
567    fn whenever_actions() {
568        assert_eq!(st("WHENEVER SQLERROR GO TO 9999-ERROR"), Statement::Whenever { condition: Condition::SqlError, action: Action::GoTo("9999-ERROR".into()) });
569        assert_eq!(st("WHENEVER NOT FOUND CONTINUE"), Statement::Whenever { condition: Condition::NotFound, action: Action::Continue });
570        assert_eq!(st("WHENEVER SQLWARNING GOTO :WARN-PARA"), Statement::Whenever { condition: Condition::SqlWarning, action: Action::GoTo("WARN-PARA".into()) });
571    }
572
573    #[test]
574    fn set_and_values_assign_through_a_query() {
575        let Statement::Query { text, into, .. } = st("SET :WS-TS = CURRENT TIMESTAMP") else { panic!() };
576        assert_eq!((text.as_str(), into[0].var.name.as_str()), ("VALUES CURRENT TIMESTAMP", "WS-TS"));
577        let Statement::Query { text, .. } = st("VALUES (:A + 1) INTO :B") else { panic!() };
578        assert_eq!(text, "VALUES (? + 1)");
579    }
580
581    #[test]
582    fn what_is_refused_and_why() {
583        assert_eq!(st("CONNECT TO DB1"), Statement::Unsupported("CONNECT".into()));
584        assert!(matches!(st("DISCONNECT ALL"), Statement::Malformed(why) if why.starts_with("DISCONNECT is not a Db2 for z/OS statement")));
585        assert_eq!(st("PREPARE S1 FROM :STMT"), Statement::Unsupported("PREPARE".into()));
586        assert_eq!(st("FETCH PRIOR FROM C1 INTO :A"), Statement::Unsupported("a scrollable FETCH".into()));
587        let prepared = Statement::DeclareUnsupported { name: "C2".into(), what: "a cursor for a prepared statement".into() };
588        assert_eq!(st("DECLARE C2 CURSOR FOR S1"), prepared);
589        assert!(matches!(st("FETCH INTO :A"), Statement::Malformed(_)));
590        assert!(matches!(st("SELECT A INTO FROM T"), Statement::Malformed(_)));
591        assert!(matches!(st("SELECT A FROM T"), Statement::Malformed(_)));
592        assert!(matches!(st("SELECT A INTO :X FROM T WHERE B = 'OPEN"), Statement::Malformed(_)));
593    }
594
595    #[test]
596    fn two_character_operators_stay_whole() {
597        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!() };
598        assert_eq!(text, "SELECT A FROM T WHERE B <= ? AND C <> 1 AND D >= 2 AND E || F = 'A < = B'");
599    }
600
601    #[test]
602    fn the_fingerprint_ignores_spelling() {
603        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!() };
604        assert_eq!(fingerprint(&a), fingerprint(&b));
605        assert_eq!(fingerprint(""), 0x811c_9dc5);
606    }
607
608    #[test]
609    fn the_parser_stamps_whenever_in_listing_order_and_numbers_blocks() {
610        let program = crate::parse(concat!(
611            "       IDENTIFICATION DIVISION.\n",
612            "       PROGRAM-ID. T.\n",
613            "       DATA DIVISION.\n",
614            "       WORKING-STORAGE SECTION.\n",
615            "       01 A PIC X(8).\n",
616            "       PROCEDURE DIVISION.\n",
617            "           PERFORM LATER.\n",
618            "           EXEC SQL SELECT X INTO :A FROM T END-EXEC.\n",
619            "           GOBACK.\n",
620            "       LATER.\n",
621            "           EXEC SQL WHENEVER NOT FOUND GO TO DONE END-EXEC.\n",
622            "           EXEC SQL SELECT Y INTO :A FROM T END-EXEC.\n",
623            "       DONE.\n",
624            "           EXIT.\n",
625        ))
626        .expect("parses");
627        let blocks: Vec<&Sql> = sql_blocks(&program);
628        assert_eq!(blocks.iter().map(|s| s.ordinal).collect::<Vec<_>>(), [1, 2, 3]);
629        assert_eq!(blocks[0].whenever.not_found, Action::Continue);
630        assert_eq!(blocks[2].whenever.not_found, Action::GoTo("DONE".into()));
631    }
632
633    #[test]
634    fn a_cursor_is_read_against_the_declares_before_it() {
635        let mut cursors = Cursors::default();
636        assert_eq!(cursors.resolve(st("FETCH C1 INTO :A")), Statement::Malformed("cursor C1 is not declared before this statement".into()));
637        let Statement::DeclareCursor(c1) = cursors.resolve(st("DECLARE C1 CURSOR WITH HOLD FOR SELECT A FROM T WHERE K = :K")) else { panic!() };
638        assert_eq!(cursors.resolve(st("OPEN C1")), Statement::Open { cursor: "C1".into(), declared: Some(c1) });
639        assert!(matches!(cursors.resolve(st("FETCH C1 INTO :A")), Statement::Fetch { .. }));
640        assert!(matches!(cursors.resolve(st("DELETE FROM T WHERE CURRENT OF C1")), Statement::Change { .. }));
641        assert!(matches!(cursors.resolve(st("UPDATE T SET A = 1 WHERE CURRENT OF C9")), Statement::Malformed(_)));
642        cursors.resolve(st("DECLARE C2 CURSOR FOR S1"));
643        assert_eq!(cursors.resolve(st("OPEN C2")), Statement::Unsupported("a cursor for a prepared statement".into()));
644    }
645
646    fn sql_blocks(program: &crate::ast::Program) -> Vec<&Sql> {
647        program
648            .paragraphs
649            .iter()
650            .flat_map(|p| &p.statements)
651            .filter_map(|s| match s {
652                crate::ast::Stmt::Exec(block) => block.sql.as_ref(),
653                _ => None,
654            })
655            .collect()
656    }
657}