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