Skip to main content

ironwork_rt/sql/
mod.rs

1//! The SQL runtime: values at the database boundary, the host variables they come from and go to,
2//! and EXEC SQL statements run against the session (`run`, over the executor's `SqlHost`). It
3//! reaches storage only as a byte range and a host type, so any executor can call it.
4
5mod convert;
6mod database;
7mod host;
8mod postgres;
9mod replay;
10mod run;
11
12pub use convert::{ReadError, Written, read, write};
13pub use database::{Abandoned, Answer, Call, Database, OpenCursor, Outcome, Session};
14pub use postgres::{Postgres, Stream, Tls};
15pub use replay::{Recorder, Replay};
16pub use run::{Ran, SqlHost, run};
17
18use crate::vocab::SignClause;
19
20/// A value at the database boundary.
21#[derive(Clone, Debug, PartialEq)]
22pub enum Value {
23    Null,
24    Int(i64),
25    /// The unscaled value and its decimal places.
26    Decimal { value: i128, scale: u32 },
27    Double(f64),
28    Char(String),
29    Binary(Vec<u8>),
30}
31
32impl Value {
33    /// The value as a recording writes it, a string without its trailing blanks; NULL as nothing.
34    pub fn text(&self) -> String {
35        match self {
36            Value::Null => String::new(),
37            Value::Int(n) => n.to_string(),
38            Value::Decimal { value, scale } => Self::decimal_text(*value, *scale),
39            Value::Double(d) => d.to_string(),
40            Value::Char(s) => s.trim_end().to_owned(),
41            Value::Binary(b) => b.iter().map(|byte| format!("{byte:02X}")).collect(),
42        }
43    }
44
45    /// A decimal's text, `-1234.50` for -123450 at scale 2, as recordings and backends write it.
46    pub fn decimal_text(value: i128, scale: u32) -> String {
47        let s = scale as usize;
48        let digits = format!("{:0>width$}", value.unsigned_abs(), width = s + 1);
49        let (whole, places) = digits.split_at(digits.len() - s);
50        let sign = if value < 0 { "-" } else { "" };
51        if s == 0 { format!("{sign}{whole}") } else { format!("{sign}{whole}.{places}") }
52    }
53
54    /// A decimal from its text, its scale the count of digits after the point.
55    pub fn parse_decimal(text: &str) -> Option<Value> {
56        let (negative, digits) = match text.strip_prefix('-') {
57            Some(d) => (true, d),
58            None => (false, text.strip_prefix('+').unwrap_or(text)),
59        };
60        let (whole, places) = digits.split_once('.').unwrap_or((digits, ""));
61        if whole.is_empty() || !whole.bytes().chain(places.bytes()).all(|b| b.is_ascii_digit()) {
62            return None;
63        }
64        let magnitude: i128 = format!("{whole}{places}").parse().ok()?;
65        Some(Value::Decimal { value: if negative { -magnitude } else { magnitude }, scale: places.len() as u32 })
66    }
67}
68
69/// An SQLCODE and its SQLSTATE.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct SqlError {
72    pub code: i32,
73    pub state: &'static str,
74}
75
76pub const NOT_ASSIGNABLE: SqlError = SqlError { code: -303, state: "42806" };
77pub const OUT_OF_RANGE: SqlError = SqlError { code: -304, state: "22003" };
78pub const NULL_WITHOUT_INDICATOR: SqlError = SqlError { code: -305, state: "22002" };
79pub const BAD_LENGTH: SqlError = SqlError { code: -311, state: "22501" };
80pub const UNCONVERTIBLE: SqlError = SqlError { code: -330, state: "22021" };
81
82/// What a host variable holds, as Db2 reads its COBOL declaration.
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub enum HostType {
85    SmallInt { signed: bool },
86    Integer { signed: bool },
87    BigInt { signed: bool },
88    Decimal { digits: u32, scale: u32, signed: bool },
89    Zoned { digits: u32, scale: u32, signed: bool, sign: Option<SignClause> },
90    Real,
91    Double,
92    Char(u32),
93    VarChar(u32),
94    /// GRAPHIC: a DBCS item of this many characters.
95    Graphic(u32),
96    /// VARGRAPHIC: a 49-level halfword of DBCS characters and a 49-level DBCS item of at most this
97    /// many.
98    VarGraphic(u32),
99    /// A host structure: its members' layout items and types, in order.
100    Structure(Vec<(usize, HostType)>),
101}
102
103/// A statement's identity in a recording: 32-bit FNV-1a over its canonical text.
104pub fn fingerprint(text: &str) -> u32 {
105    text.bytes().fold(0x811c_9dc5, |h, b| (h ^ u32::from(b)).wrapping_mul(0x0100_0193))
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn the_fingerprint_of_nothing_is_the_fnv_offset() {
114        assert_eq!(fingerprint(""), 0x811c_9dc5);
115        assert_ne!(fingerprint("COMMIT"), fingerprint("ROLLBACK"));
116    }
117}