Skip to main content

ironwork_rt/sql/
replay.rs

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