1use super::{Abandoned, Answer, Call, Column, ColumnType, Database, Outcome, Value};
19use std::io::Write;
20use super::fingerprint;
21
22const HEADER: &str = "# ironwork sql recording 1";
23
24#[derive(Debug)]
25struct Entry {
26 program: String,
27 ordinal: u32,
28 hash: u32,
29 verb: String,
30 cursor: Option<String>,
31 inputs: Vec<Value>,
32 outcome: Outcome,
33}
34
35impl Entry {
36 fn answers(&self, call: &Call) -> bool {
37 self.program == call.program
38 && self.ordinal == call.ordinal
39 && self.hash == fingerprint(call.text)
40 && self.verb == call.verb
41 && self.cursor.as_deref() == call.cursor
42 && self.inputs == call.inputs
43 }
44}
45
46fn describe(program: &str, ordinal: u32, hash: u32, verb: &str, cursor: Option<&str>, inputs: &[Value]) -> String {
47 let cursor = cursor.map(|c| format!(" {c}")).unwrap_or_default();
48 let inputs = if inputs.is_empty() { String::new() } else { format!(" with {}", values_text(inputs)) };
49 format!("{program}:{ordinal}:{hash:08x} {verb}{cursor}{inputs}")
50}
51
52pub struct Replay {
56 entries: Vec<Entry>,
57 used: Vec<bool>,
58 next: usize,
59 keyed: bool,
60}
61
62impl Replay {
63 pub fn parse(text: &str, keyed: bool) -> Result<Self, String> {
64 let mut entries: Vec<Entry> = Vec::new();
65 let mut header = false;
66 let mut awaiting_outcome = false;
67 for (n, line) in text.lines().enumerate().map(|(i, l)| (i + 1, l.trim_end())) {
68 let fail = |why: String| format!("line {n}: {why}");
69 if line.is_empty() {
70 continue;
71 }
72 if line.starts_with('#') {
73 header |= line == HEADER;
74 continue;
75 }
76 if !header {
77 return Err(fail(format!("a recording starts with \"{HEADER}\"")));
78 }
79 let (mark, rest) = line.split_at(1);
80 let rest = rest.trim_start();
81 match mark {
82 "@" => {
83 if awaiting_outcome {
84 return Err(fail("the call before this one has no < line".into()));
85 }
86 let words: Vec<&str> = rest.split_whitespace().collect();
87 let [_, id, verb, cursor @ ..] = words.as_slice() else { return Err(fail("@ takes a number, PROGRAM:ORDINAL:HASH and a verb".into())) };
88 let parts: Vec<&str> = id.split(':').collect();
89 let [program, ordinal, hash] = parts.as_slice() else { return Err(fail(format!("{id} is not PROGRAM:ORDINAL:HASH"))) };
90 let ordinal = ordinal.parse().map_err(|_| fail(format!("{ordinal} is not an ordinal")))?;
91 let hash = u32::from_str_radix(hash, 16).map_err(|_| fail(format!("{hash} is not a hexadecimal hash")))?;
92 let cursor = match cursor {
93 [] => None,
94 [c] => Some((*c).to_owned()),
95 _ => return Err(fail("@ takes at most one cursor after the verb".into())),
96 };
97 entries.push(Entry { program: (*program).into(), ordinal, hash, verb: (*verb).into(), cursor, inputs: Vec::new(), outcome: Outcome::ok() });
98 awaiting_outcome = true;
99 }
100 ">" => match entries.last_mut() {
101 Some(e) if awaiting_outcome => e.inputs.extend(parse_values(rest).map_err(fail)?),
102 _ => return Err(fail("a > line belongs after an @ line, before its < line".into())),
103 },
104 "<" => match entries.last_mut() {
105 Some(e) if awaiting_outcome => {
106 e.outcome = parse_outcome(rest).map_err(fail)?;
107 awaiting_outcome = false;
108 }
109 _ => return Err(fail("a < line belongs after an @ line".into())),
110 },
111 "=" => match entries.last_mut() {
112 Some(e) if !awaiting_outcome && e.verb == "CALL" && e.outcome.parameters.is_empty() => e.outcome.parameters = parse_parameters(rest).map_err(fail)?,
113 Some(e) if !awaiting_outcome && e.verb == "CALL" => return Err(fail("a CALL has one = line".into())),
114 Some(e) if !awaiting_outcome => e.outcome.rows.push(parse_values(rest).map_err(fail)?),
115 _ => return Err(fail("an = line belongs after a < line".into())),
116 },
117 ":" => match entries.last_mut() {
118 Some(e) if !awaiting_outcome => e.outcome.columns.push(parse_column(rest).map_err(fail)?),
119 _ => return Err(fail("a : line belongs after a < line".into())),
120 },
121 _ => return Err(fail(format!("{mark} starts no kind of line"))),
122 }
123 }
124 if awaiting_outcome {
125 return Err("the last call has no < line".into());
126 }
127 if !header {
128 return Err(format!("a recording starts with \"{HEADER}\""));
129 }
130 let used = vec![false; entries.len()];
131 Ok(Self { entries, used, next: 0, keyed })
132 }
133
134 fn answer(&mut self, call: &Call) -> Answer {
135 let found = if self.keyed {
136 (0..self.entries.len()).find(|&i| !self.used[i] && self.entries[i].answers(call))
137 } else {
138 (self.next < self.entries.len() && self.entries[self.next].answers(call)).then_some(self.next)
139 };
140 let actual = describe(call.program, call.ordinal, fingerprint(call.text), call.verb, call.cursor, call.inputs);
141 let Some(i) = found else {
142 let expected = match self.entries.get(self.next).filter(|_| !self.keyed) {
143 Some(e) => format!("the recording's call {} is {}", self.next + 1, describe(&e.program, e.ordinal, e.hash, &e.verb, e.cursor.as_deref(), &e.inputs)),
144 None => "the recording holds no such call".into(),
145 };
146 return Err(Abandoned { code: "SQLR", message: format!("{expected}, and the run made {actual}") });
147 };
148 self.used[i] = true;
149 self.next = i + 1;
150 Ok(self.entries[i].outcome.clone())
151 }
152}
153
154impl Database for Replay {
155 fn execute(&mut self, call: &Call) -> Answer {
156 self.answer(call)
157 }
158 fn prepare(&mut self, call: &Call) -> Answer {
159 self.answer(call)
160 }
161 fn open(&mut self, call: &Call) -> Answer {
162 self.answer(call)
163 }
164 fn fetch(&mut self, call: &Call) -> Answer {
165 self.answer(call)
166 }
167 fn fetch_rows(&mut self, call: &Call, _: u32) -> Answer {
168 self.answer(call)
169 }
170 fn insert_rows(&mut self, call: &Call, _: &[Vec<Value>], _: bool) -> Answer {
171 self.answer(call)
172 }
173 fn call(&mut self, call: &Call) -> Answer {
174 self.answer(call)
175 }
176 fn close(&mut self, call: &Call) -> Answer {
177 self.answer(call)
178 }
179 fn commit(&mut self, call: &Call) -> Answer {
180 self.answer(call)
181 }
182 fn rollback(&mut self, call: &Call) -> Answer {
183 self.answer(call)
184 }
185}
186
187pub struct Recorder<'w> {
189 inner: Box<dyn Database + 'w>,
190 out: Box<dyn Write + 'w>,
191 seq: u64,
192}
193
194impl<'w> Recorder<'w> {
195 pub fn new(inner: Box<dyn Database + 'w>, mut out: Box<dyn Write + 'w>, source: &str) -> std::io::Result<Self> {
197 writeln!(out, "{HEADER}\n# source: {source}")?;
198 Ok(Self { inner, out, seq: 0 })
199 }
200
201 fn record(&mut self, call: &Call, answer: Answer) -> Answer {
202 self.record_rows(call, None, answer)
203 }
204
205 fn record_rows(&mut self, call: &Call, rows: Option<&[Vec<Value>]>, answer: Answer) -> Answer {
207 let outcome = answer?;
208 self.seq += 1;
209 let text = entry_text(self.seq, call, rows, &outcome);
210 self.out.write_all(text.as_bytes()).and_then(|()| self.out.flush()).map_err(|e| Abandoned { code: "SQLR", message: format!("the recording could not be written: {e}") })?;
212 Ok(outcome)
213 }
214}
215
216impl Database for Recorder<'_> {
217 fn execute(&mut self, call: &Call) -> Answer {
218 let a = self.inner.execute(call);
219 self.record(call, a)
220 }
221 fn prepare(&mut self, call: &Call) -> Answer {
222 let a = self.inner.prepare(call);
223 self.record(call, a)
224 }
225 fn open(&mut self, call: &Call) -> Answer {
226 let a = self.inner.open(call);
227 self.record(call, a)
228 }
229 fn fetch(&mut self, call: &Call) -> Answer {
230 let a = self.inner.fetch(call);
231 self.record(call, a)
232 }
233 fn fetch_rows(&mut self, call: &Call, rows: u32) -> Answer {
234 let a = self.inner.fetch_rows(call, rows);
235 self.record(call, a)
236 }
237 fn insert_rows(&mut self, call: &Call, rows: &[Vec<Value>], atomic: bool) -> Answer {
238 let a = self.inner.insert_rows(call, rows, atomic);
239 self.record_rows(call, Some(rows), a)
240 }
241 fn call(&mut self, call: &Call) -> Answer {
242 let a = self.inner.call(call);
243 self.record(call, a)
244 }
245 fn close(&mut self, call: &Call) -> Answer {
246 let a = self.inner.close(call);
247 self.record(call, a)
248 }
249 fn commit(&mut self, call: &Call) -> Answer {
250 let a = self.inner.commit(call);
251 self.record(call, a)
252 }
253 fn rollback(&mut self, call: &Call) -> Answer {
254 let a = self.inner.rollback(call);
255 self.record(call, a)
256 }
257 fn close_all(&mut self) -> Result<(), Abandoned> {
258 self.inner.close_all()
259 }
260}
261
262fn entry_text(seq: u64, call: &Call, rows: Option<&[Vec<Value>]>, outcome: &Outcome) -> String {
263 let cursor = call.cursor.map(|c| format!(" {c}")).unwrap_or_default();
264 let mut text = format!("@ {seq} {}:{}:{:08x} {}{cursor}\n", call.program, call.ordinal, fingerprint(call.text), call.verb);
265 match rows {
266 Some(rows) => rows.iter().for_each(|row| text += &format!("> {}\n", values_text(row))),
267 None if !call.inputs.is_empty() => text += &format!("> {}\n", values_text(call.inputs)),
268 None => {}
269 }
270 text += &format!("< {} {} rows={}", outcome.sqlcode, outcome.sqlstate, outcome.affected);
271 if !outcome.tokens.is_empty() {
272 text += &format!(" tokens={}", value_text(&Value::Char(outcome.tokens.clone())));
273 }
274 text.push('\n');
275 for column in &outcome.columns {
276 text += &format!(": {}\n", column_text(column));
277 }
278 for row in &outcome.rows {
279 text += &format!("= {}\n", values_text(row));
280 }
281 if !outcome.parameters.is_empty() {
282 text += &format!("= {}\n", outcome.parameters.iter().map(|p| p.as_ref().map_or_else(|| "-".to_owned(), value_text)).collect::<Vec<_>>().join(" | "));
283 }
284 text
285}
286
287fn column_text(c: &Column) -> String {
289 let ty = match &c.ty {
290 ColumnType::Char(n) => format!("char({n})"),
291 ColumnType::VarChar(n) => format!("varchar({n})"),
292 ColumnType::Graphic(n) => format!("graphic({n})"),
293 ColumnType::VarGraphic(n) => format!("vargraphic({n})"),
294 ColumnType::SmallInt => "smallint".into(),
295 ColumnType::Integer => "integer".into(),
296 ColumnType::BigInt => "bigint".into(),
297 ColumnType::Decimal { precision, scale } => format!("decimal({precision},{scale})"),
298 ColumnType::Real => "real".into(),
299 ColumnType::Double => "double".into(),
300 ColumnType::Date => "date".into(),
301 ColumnType::Time => "time".into(),
302 ColumnType::Timestamp(p) => format!("timestamp({p})"),
303 ColumnType::Binary(n) => format!("binary({n})"),
304 ColumnType::VarBinary(n) => format!("varbinary({n})"),
305 ColumnType::Other(name) => format!("other:{}", value_text(&Value::Char(name.clone()))),
306 };
307 format!("{} {ty} {}", value_text(&Value::Char(c.name.clone())), if c.nullable { "null" } else { "notnull" })
308}
309
310fn parse_column(text: &str) -> Result<Column, String> {
311 let shape = "a : line is a char:\"name\", a type and null or notnull";
312 let Ok((Value::Char(name), rest)) = parse_value(text.trim_start()) else { return Err(shape.into()) };
313 let rest = rest.trim();
314 let (ty, nullable) = match rest.rsplit_once(' ') {
315 Some((ty, "null")) => (ty.trim(), true),
316 Some((ty, "notnull")) => (ty.trim(), false),
317 _ => return Err(shape.into()),
318 };
319 let sized = |inner: &str| inner.parse::<u16>().map_err(|_| format!("{ty} has no length"));
320 let ty = match ty.split_once('(').map(|(w, r)| (w, r.strip_suffix(')'))) {
321 Some(("char", Some(n))) => ColumnType::Char(sized(n)?),
322 Some(("varchar", Some(n))) => ColumnType::VarChar(sized(n)?),
323 Some(("graphic", Some(n))) => ColumnType::Graphic(sized(n)?),
324 Some(("vargraphic", Some(n))) => ColumnType::VarGraphic(sized(n)?),
325 Some(("binary", Some(n))) => ColumnType::Binary(sized(n)?),
326 Some(("varbinary", Some(n))) => ColumnType::VarBinary(sized(n)?),
327 Some(("timestamp", Some(p))) => ColumnType::Timestamp(p.parse().map_err(|_| format!("{ty} has no precision"))?),
328 Some(("decimal", Some(ps))) => match ps.split_once(',').map(|(p, s)| (p.parse(), s.parse())) {
329 Some((Ok(precision), Ok(scale))) => ColumnType::Decimal { precision, scale },
330 _ => return Err(format!("{ty} is not decimal(p,s)")),
331 },
332 _ => match ty {
333 "smallint" => ColumnType::SmallInt,
334 "integer" => ColumnType::Integer,
335 "bigint" => ColumnType::BigInt,
336 "real" => ColumnType::Real,
337 "double" => ColumnType::Double,
338 "date" => ColumnType::Date,
339 "time" => ColumnType::Time,
340 other => match other.strip_prefix("other:").map(parse_value) {
341 Some(Ok((Value::Char(name), ""))) => ColumnType::Other(name),
342 _ => return Err(format!("{other} is not a column type")),
343 },
344 },
345 };
346 Ok(Column { name, ty, nullable })
347}
348
349fn values_text(values: &[Value]) -> String {
350 values.iter().map(value_text).collect::<Vec<_>>().join(" | ")
351}
352
353fn value_text(v: &Value) -> String {
354 match v {
355 Value::Null => "null".into(),
356 Value::Int(i) => format!("int:{i}"),
357 Value::Decimal { value, scale } => format!("dec:{}", Value::decimal_text(*value, *scale)),
358 Value::Double(f) => format!("double:{f:?}"),
359 Value::Char(s) => {
360 let mut q = String::from("char:\"");
361 for c in s.chars() {
362 match c {
363 '"' => q += "\\\"",
364 '\\' => q += "\\\\",
365 c if c.is_control() => q += &format!("\\x{:02X}", c as u32),
366 c => q.push(c),
367 }
368 }
369 q + "\""
370 }
371 Value::Binary(b) => format!("hex:{}", b.iter().map(|x| format!("{x:02X}")).collect::<String>()),
372 }
373}
374
375fn parse_outcome(text: &str) -> Result<Outcome, String> {
376 let mut words = text.splitn(4, ' ');
377 let (Some(code), Some(state), Some(rows)) = (words.next(), words.next(), words.next()) else { return Err("< takes SQLCODE, SQLSTATE and rows=N".into()) };
378 let sqlcode = code.parse().map_err(|_| format!("{code} is not an SQLCODE"))?;
379 if state.len() != 5 {
380 return Err(format!("{state} is not a five-character SQLSTATE"));
381 }
382 let affected = rows.strip_prefix("rows=").and_then(|n| n.parse().ok()).ok_or_else(|| format!("{rows} is not rows=N"))?;
383 let tokens = match words.next().map(str::trim) {
384 None | Some("") => String::new(),
385 Some(t) => match t.strip_prefix("tokens=").map(parse_value) {
386 Some(Ok((Value::Char(s), rest))) if rest.trim().is_empty() => s,
387 _ => return Err("the rest of a < line is tokens=char:\"...\"".into()),
388 },
389 };
390 Ok(Outcome { sqlcode, sqlstate: state.into(), affected, rows: Vec::new(), tokens, columns: Vec::new(), parameters: Vec::new() })
391}
392
393fn parse_parameters(text: &str) -> Result<Vec<Option<Value>>, String> {
395 parse_list(text, |item| match item.strip_prefix('-') {
396 Some(rest) if rest.trim_start().is_empty() || rest.trim_start().starts_with('|') => Ok((None, rest)),
397 _ => parse_value(item).map(|(v, rest)| (Some(v), rest)),
398 })
399}
400
401fn parse_values(text: &str) -> Result<Vec<Value>, String> {
402 parse_list(text, parse_value)
403}
404
405fn parse_list<T>(text: &str, item: impl Fn(&str) -> Result<(T, &str), String>) -> Result<Vec<T>, String> {
407 let (mut out, mut rest) = (Vec::new(), text.trim_start());
408 while !rest.is_empty() {
409 let (v, after) = item(rest)?;
410 out.push(v);
411 rest = after.trim_start();
412 if let Some(next) = rest.strip_prefix('|') {
413 rest = next.trim_start();
414 if rest.is_empty() {
415 return Err("a value is missing after |".into());
416 }
417 } else if !rest.is_empty() {
418 return Err(format!("values are separated by |, not \"{rest}\""));
419 }
420 }
421 Ok(out)
422}
423
424fn parse_value(text: &str) -> Result<(Value, &str), String> {
426 if let Some(quoted) = text.strip_prefix("char:\"") {
427 let mut s = String::new();
428 let mut chars = quoted.char_indices();
429 while let Some((i, c)) = chars.next() {
430 match c {
431 '"' => return Ok((Value::Char(s), "ed[i + 1..])),
432 '\\' => match chars.next() {
433 Some((_, '"')) => s.push('"'),
434 Some((_, '\\')) => s.push('\\'),
435 Some((j, 'x')) => {
436 let hex = quoted.get(j + 1..j + 3).ok_or("\\x takes two hexadecimal digits")?;
437 let code = u32::from_str_radix(hex, 16).map_err(|_| format!("\\x{hex} is not hexadecimal"))?;
438 s.push(char::from_u32(code).ok_or("\\x names no character")?);
439 chars.next();
440 chars.next();
441 }
442 _ => return Err("a backslash in char:\"...\" escapes \", \\ or xNN".into()),
443 },
444 c => s.push(c),
445 }
446 }
447 return Err("char:\" is not closed".into());
448 }
449 let end = text.find(|c: char| c.is_whitespace() || c == '|').unwrap_or(text.len());
450 let (word, rest) = text.split_at(end);
451 let value = match word.split_once(':') {
452 None if word == "null" => Value::Null,
453 Some(("int", n)) => Value::Int(n.parse().map_err(|_| format!("{word} is not an integer"))?),
454 Some(("dec", n)) => Value::parse_decimal(n).ok_or_else(|| format!("{word} is not a decimal"))?,
455 Some(("double", n)) => Value::Double(n.parse().map_err(|_| format!("{word} is not a double"))?),
456 Some(("hex", h)) if h.len() % 2 == 0 => {
457 let bytes: Result<Vec<u8>, _> = (0..h.len()).step_by(2).map(|i| u8::from_str_radix(&h[i..i + 2], 16)).collect();
458 Value::Binary(bytes.map_err(|_| format!("{word} is not hexadecimal"))?)
459 }
460 _ => return Err(format!("{word} is not a value: null, int:, dec:, double:, char:\"...\" or hex:")),
461 };
462 Ok((value, rest))
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 fn call<'a>(verb: &'a str, text: &'a str, inputs: &'a [Value]) -> Call<'a> {
470 Call { program: "P", ordinal: 2, verb, cursor: None, text, inputs }
471 }
472
473 #[test]
474 fn values_round_trip() {
475 let values = vec![
476 Value::Null,
477 Value::Int(-42),
478 Value::Decimal { value: -123_450, scale: 2 },
479 Value::Decimal { value: 5, scale: 2 },
480 Value::Decimal { value: 7, scale: 0 },
481 Value::Double(0.1),
482 Value::Double(6.02e23),
483 Value::Char("say \"hi\" | x \\ é\n".into()),
484 Value::Binary(vec![0xC1, 0x00]),
485 ];
486 let text = values_text(&values);
487 assert!(text.contains("dec:-1234.50") && text.contains("dec:0.05") && text.contains("dec:7"), "{text}");
488 assert_eq!(parse_values(&text), Ok(values));
489 }
490
491 #[test]
492 fn a_recording_answers_the_calls_it_holds_in_order() {
493 let inputs = [Value::Int(7)];
494 let first = entry_text(1, &call("SELECT", "SELECT A FROM T WHERE K = ?", &inputs), None, &Outcome::rows(vec![vec![Value::Char("X".into())]]));
495 let second = entry_text(2, &call("COMMIT", "COMMIT", &[]), None, &Outcome::ok());
496 let mut replay = Replay::parse(&format!("{HEADER}\n{first}{second}"), false).expect("parses");
497 assert_eq!(replay.execute(&call("SELECT", "SELECT A FROM T WHERE K = ?", &inputs)).unwrap().rows, [[Value::Char("X".into())]]);
498 assert_eq!(replay.commit(&call("COMMIT", "COMMIT", &[])), Ok(Outcome::ok()));
499 let beyond = replay.commit(&call("COMMIT", "COMMIT", &[])).unwrap_err();
500 assert_eq!((beyond.code, beyond.message.starts_with("the recording holds no such call")), ("SQLR", true));
501 }
502
503 #[test]
504 fn strict_replay_refuses_a_different_call_and_names_both() {
505 let text = format!("{HEADER}\n{}", entry_text(1, &call("SELECT", "SELECT A FROM T WHERE K = ?", &[Value::Int(7)]), None, &Outcome::ok()));
506 let mut replay = Replay::parse(&text, false).unwrap();
507 let err = replay.execute(&call("SELECT", "SELECT A FROM T WHERE K = ?", &[Value::Int(8)])).unwrap_err();
508 assert_eq!(err.code, "SQLR");
509 assert!(err.message.contains("with int:7") && err.message.contains("with int:8"), "{}", err.message);
510 }
511
512 #[test]
513 fn keyed_replay_takes_calls_in_any_order() {
514 let (a, b) = ([Value::Int(1)], [Value::Int(2)]);
515 let text = format!(
516 "{HEADER}\n{}{}",
517 entry_text(1, &call("SELECT", "Q", &a), None, &Outcome::rows(vec![vec![Value::Int(10)]])),
518 entry_text(2, &call("SELECT", "Q", &b), None, &Outcome::rows(vec![vec![Value::Int(20)]]))
519 );
520 let mut replay = Replay::parse(&text, true).unwrap();
521 assert_eq!(replay.execute(&call("SELECT", "Q", &b)).unwrap().rows, [[Value::Int(20)]]);
522 assert_eq!(replay.execute(&call("SELECT", "Q", &a)).unwrap().rows, [[Value::Int(10)]]);
523 assert!(replay.execute(&call("SELECT", "Q", &a)).is_err());
524 }
525
526 #[test]
527 fn a_recorder_writes_what_replay_reads() {
528 struct Fixed;
529 impl Database for Fixed {
530 fn execute(&mut self, _: &Call) -> Answer {
531 Ok(Outcome { tokens: "T1".into(), ..Outcome::rows(vec![vec![Value::Decimal { value: 150, scale: 2 }, Value::Null]]) })
532 }
533 fn prepare(&mut self, _: &Call) -> Answer {
534 let columns = vec![
535 Column { name: "NAME".into(), ty: ColumnType::Char(10), nullable: false },
536 Column { name: "AMT".into(), ty: ColumnType::Decimal { precision: 7, scale: 2 }, nullable: true },
537 Column { name: "ODD \"ONE\"".into(), ty: ColumnType::Other("PostgreSQL type OID 16".into()), nullable: true },
538 Column { name: "TS".into(), ty: ColumnType::Timestamp(6), nullable: true },
539 ];
540 Ok(Outcome { columns, ..Outcome::ok() })
541 }
542 fn open(&mut self, _: &Call) -> Answer {
543 Ok(Outcome::ok())
544 }
545 fn fetch(&mut self, _: &Call) -> Answer {
546 Ok(Outcome::error(100, "02000"))
547 }
548 fn fetch_rows(&mut self, _: &Call, _: u32) -> Answer {
549 Ok(Outcome { affected: 2, ..Outcome::rows(vec![vec![Value::Int(1)], vec![Value::Int(2)]]) })
550 }
551 fn insert_rows(&mut self, _: &Call, rows: &[Vec<Value>], _: bool) -> Answer {
552 Ok(Outcome { affected: rows.len() as i64, ..Outcome::ok() })
553 }
554 fn call(&mut self, _: &Call) -> Answer {
555 Ok(Outcome { parameters: vec![None, Some(Value::Char("A|B".into())), Some(Value::Null)], ..Outcome::error(466, "0100C") })
556 }
557 fn close(&mut self, _: &Call) -> Answer {
558 Ok(Outcome::ok())
559 }
560 fn commit(&mut self, _: &Call) -> Answer {
561 Ok(Outcome::ok())
562 }
563 fn rollback(&mut self, _: &Call) -> Answer {
564 Ok(Outcome::ok())
565 }
566 }
567 let written = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
568 struct Sink(std::rc::Rc<std::cell::RefCell<Vec<u8>>>);
569 impl Write for Sink {
570 fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
571 self.0.borrow_mut().extend_from_slice(b);
572 Ok(b.len())
573 }
574 fn flush(&mut self) -> std::io::Result<()> {
575 Ok(())
576 }
577 }
578 let inputs = [Value::Char("A|B".into())];
579 let mut recorder = Recorder::new(Box::new(Fixed), Box::new(Sink(written.clone())), "a test double").unwrap();
580 let live = recorder.execute(&call("SELECT", "SELECT X, Y FROM T WHERE Z = ?", &inputs)).unwrap();
581 let text = String::from_utf8(written.borrow().clone()).unwrap();
582 assert!(text.starts_with(&format!("{HEADER}\n# source: a test double\n")), "{text}");
583 let prepared = recorder.prepare(&Call { cursor: Some("S1"), ..call("PREPARE", "SELECT NAME, AMT FROM T", &[]) }).unwrap();
584 let text = String::from_utf8(written.borrow().clone()).unwrap();
585 assert!(text.contains("PREPARE S1\n< 0 00000 rows=0\n: char:\"NAME\" char(10) notnull\n: char:\"AMT\" decimal(7,2) null\n"), "{text}");
586 let mut replay = Replay::parse(&text, false).unwrap();
587 assert_eq!(replay.execute(&call("SELECT", "SELECT X, Y FROM T WHERE Z = ?", &inputs)), Ok(live));
588 assert_eq!(replay.prepare(&Call { cursor: Some("S1"), ..call("PREPARE", "SELECT NAME, AMT FROM T", &[]) }), Ok(prepared));
589
590 written.borrow_mut().clear();
591 let mut recorder = Recorder::new(Box::new(Fixed), Box::new(Sink(written.clone())), "a test double").unwrap();
592 let rows = [vec![Value::Int(1), Value::Char("X".into())], vec![Value::Int(2), Value::Null]];
593 let flat = rows.concat();
594 let insert = Call { cursor: None, ..call("INSERT", "INSERT INTO T VALUES (?, ?)", &flat) };
595 let fetch = Call { cursor: Some("C1"), ..call("FETCH", "FETCH NEXT ROWSET FROM C1 FOR ? ROWS", &[Value::Int(2)]) };
596 let procedure = Call { cursor: Some("P1"), ..call("CALL", "CALL P1 (?, ?, ?)", &inputs) };
597 let live = [recorder.insert_rows(&insert, &rows, true), recorder.fetch_rows(&fetch, 2), recorder.call(&procedure)];
598 let text = String::from_utf8(written.borrow().clone()).unwrap();
599 assert!(text.contains("> int:1 | char:\"X\"\n> int:2 | null\n"), "{text}");
600 assert!(text.contains("= - | char:\"A|B\" | null\n"), "{text}");
601 let mut replay = Replay::parse(&text, false).unwrap();
602 assert_eq!([replay.insert_rows(&insert, &rows, true), replay.fetch_rows(&fetch, 2), replay.call(&procedure)], live);
603 }
604
605 #[test]
606 fn malformed_recordings_name_the_line() {
607 assert_eq!(Replay::parse("@ 1 P:1:0 SELECT\n< 0 00000 rows=0\n", false).err().unwrap(), format!("line 1: a recording starts with \"{HEADER}\""));
608 let err = Replay::parse(&format!("{HEADER}\n@ 1 P:1:0 SELECT\n= int:1\n"), false).err().unwrap();
609 assert_eq!(err, "line 3: an = line belongs after a < line");
610 assert!(Replay::parse(&format!("{HEADER}\n@ 1 P:1:0 SELECT\n< 0 00000 rows=0\n= int:x\n"), false).err().unwrap().starts_with("line 4: "));
611 assert_eq!(Replay::parse(&format!("{HEADER}\n@ 1 P:1:0 SELECT\n"), false).err().unwrap(), "the last call has no < line");
612 let call_lines = |equals: &str| Replay::parse(&format!("{HEADER}\n@ 1 P:1:0 CALL P\n< 0 00000 rows=0\n{equals}"), false).err();
613 assert_eq!(call_lines("= - | int:1\n= int:2\n").unwrap(), "line 5: a CALL has one = line");
614 assert!(call_lines("= -- | int:1\n").unwrap().starts_with("line 4: "));
615 assert_eq!(call_lines("= - | int:1\n"), None);
616 }
617}