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