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