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 Unsupported(String),
84 Malformed(String),
87}
88
89impl Statement {
90 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#[derive(Debug, Default)]
105pub struct Cursors(HashMap<String, Result<Cursor, String>>);
106
107impl Cursors {
108 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#[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
142pub 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 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
253fn 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
264fn 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
293fn 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 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
317fn 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}