Skip to main content

ironwork_rt/sql/
mod.rs

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