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