Skip to main content

ironwork_rt/sql/
convert.rs

1//! Host variables to values and back, by Db2's assignment rules: a number that does not fit its
2//! host variable is an error rather than a truncation, and a string that does not fit is cut and
3//! reported.
4
5use super::{BAD_LENGTH, HostType, NOT_ASSIGNABLE, OUT_OF_RANGE, SqlError, UNCONVERTIBLE, Value};
6use crate::codec;
7use crate::vocab::{SignClause, SignPosition};
8use numeric::Numproc;
9use zarch::check::ProgramCheck;
10use zarch::decimal::{self, Decimal};
11use zarch::ebcdic::CodePage;
12use zarch::hfp::{Hfp, Precision};
13
14/// Why an input host variable could not be read: its storage failed as the machine's own read
15/// would (S0C7), or Db2 refuses it with an SQLCODE.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum ReadError {
18    Check(ProgramCheck),
19    Sql(SqlError),
20}
21
22/// What storing an output value did besides store it: the length a cut string had, for SQLWARN1
23/// and the indicator.
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
25pub struct Written {
26    pub truncated_from: Option<usize>,
27}
28
29/// The value an input host variable sends. `bytes` is the host variable's storage.
30pub fn read(bytes: &[u8], ty: &HostType, page: &CodePage, numproc: Numproc) -> Result<Value, ReadError> {
31    let as_value = |d: Decimal, scale: u32| {
32        let magnitude = d.magnitude as i128;
33        Value::Decimal { value: if d.negative { -magnitude } else { magnitude }, scale }
34    };
35    Ok(match *ty {
36        HostType::SmallInt { signed } | HostType::Integer { signed } | HostType::BigInt { signed } => {
37            let v = integer(bytes, signed);
38            i64::try_from(v).map_or(Value::Decimal { value: v, scale: 0 }, Value::Int)
39        }
40        HostType::Decimal { scale, signed, .. } => as_value(codec::packed(bytes, signed, numproc).map_err(ReadError::Check)?, scale),
41        HostType::Zoned { scale, signed, sign, .. } => as_value(codec::zoned(bytes, signed, sign, numproc).map_err(ReadError::Check)?, scale),
42        HostType::Real => Value::Double(Hfp::from_bytes(Precision::Short, bytes).approx()),
43        HostType::Double => Value::Double(Hfp::from_bytes(Precision::Long, bytes).approx()),
44        HostType::Char(_) => Value::Char(page.decode(bytes)),
45        HostType::VarChar(max) => {
46            let len = i16::from_be_bytes([bytes[0], bytes[1]]);
47            if len < 0 || len as u32 > max {
48                return Err(ReadError::Sql(BAD_LENGTH));
49            }
50            Value::Char(page.decode(&bytes[2..2 + len as usize]))
51        }
52        HostType::Structure(_) => unreachable!("a host structure is read member by member"),
53    })
54}
55
56/// Stores a non-null output value into a host variable's storage, as Db2 assigns it.
57pub fn write(value: &Value, bytes: &mut [u8], ty: &HostType, page: &CodePage) -> Result<Written, SqlError> {
58    match *ty {
59        HostType::SmallInt { signed } | HostType::Integer { signed } | HostType::BigInt { signed } => store_integer(whole(value)?, bytes, signed)?,
60        HostType::Decimal { digits, scale, signed } => {
61            let v = fitted(scaled(value, scale)?, digits, signed)?;
62            decimal::encode(bytes, Decimal { negative: v < 0, magnitude: v.unsigned_abs() }).expect("a packed host variable is 1 to 16 bytes");
63            if !signed {
64                *bytes.last_mut().unwrap() |= 0x0F;
65            }
66        }
67        HostType::Zoned { digits, scale, signed, sign } => store_zoned(fitted(scaled(value, scale)?, digits, signed)?, bytes, digits, signed, sign),
68        HostType::Real => bytes.copy_from_slice(&hfp_image(float(value)?, Precision::Short).ok_or(OUT_OF_RANGE)?),
69        HostType::Double => bytes.copy_from_slice(&hfp_image(float(value)?, Precision::Long).ok_or(OUT_OF_RANGE)?),
70        HostType::Char(_) => return store_text(&text(value, page)?, bytes, page),
71        HostType::VarChar(max) => {
72            let t = text(value, page)?;
73            let n = t.len().min(max as usize);
74            bytes[..2].copy_from_slice(&(n as u16).to_be_bytes());
75            bytes[2..2 + n].copy_from_slice(&t[..n]);
76            return Ok(Written { truncated_from: (t.len() > n).then_some(t.len()) });
77        }
78        HostType::Structure(_) => unreachable!("a host structure is written member by member"),
79    }
80    Ok(Written::default())
81}
82
83fn integer(bytes: &[u8], signed: bool) -> i128 {
84    let unsigned = bytes.iter().fold(0u128, |acc, &b| acc << 8 | u128::from(b)) as i128;
85    if signed && bytes[0] & 0x80 != 0 { unsigned - (1i128 << (8 * bytes.len())) } else { unsigned }
86}
87
88fn pow10(n: u32) -> Option<i128> {
89    10i128.checked_pow(n)
90}
91
92/// The value as a whole number, fractional digits dropped.
93fn whole(value: &Value) -> Result<i128, SqlError> {
94    match *value {
95        Value::Int(v) => Ok(v.into()),
96        Value::Decimal { value, scale } => Ok(value / pow10(scale).ok_or(OUT_OF_RANGE)?),
97        Value::Double(f) if f.is_finite() && f.abs() < 1e38 => Ok(f.trunc() as i128),
98        Value::Double(_) => Err(OUT_OF_RANGE),
99        _ => Err(NOT_ASSIGNABLE),
100    }
101}
102
103/// The value unscaled at `scale` places, extra places dropped.
104fn scaled(value: &Value, scale: u32) -> Result<i128, SqlError> {
105    match *value {
106        Value::Int(v) => i128::from(v).checked_mul(pow10(scale).ok_or(OUT_OF_RANGE)?).ok_or(OUT_OF_RANGE),
107        Value::Decimal { value, scale: from } if from >= scale => Ok(value / pow10(from - scale).ok_or(OUT_OF_RANGE)?),
108        Value::Decimal { value, scale: from } => value.checked_mul(pow10(scale - from).ok_or(OUT_OF_RANGE)?).ok_or(OUT_OF_RANGE),
109        Value::Double(f) => {
110            let v = (f * 10f64.powi(scale as i32)).trunc();
111            if v.is_finite() && v.abs() < 1e38 { Ok(v as i128) } else { Err(OUT_OF_RANGE) }
112        }
113        _ => Err(NOT_ASSIGNABLE),
114    }
115}
116
117fn fitted(v: i128, digits: u32, signed: bool) -> Result<i128, SqlError> {
118    if (!signed && v < 0) || v.unsigned_abs() >= pow10(digits).ok_or(OUT_OF_RANGE)?.unsigned_abs() { Err(OUT_OF_RANGE) } else { Ok(v) }
119}
120
121fn store_integer(v: i128, bytes: &mut [u8], signed: bool) -> Result<(), SqlError> {
122    let bits = 8 * bytes.len() as u32;
123    let (low, high) = if signed { (-(1i128 << (bits - 1)), (1i128 << (bits - 1)) - 1) } else { (0, (1i128 << bits) - 1) };
124    if !(low..=high).contains(&v) {
125        return Err(OUT_OF_RANGE);
126    }
127    let image = v as u128;
128    for (i, b) in bytes.iter_mut().rev().enumerate() {
129        *b = (image >> (8 * i)) as u8;
130    }
131    Ok(())
132}
133
134/// Zoned digits through UNPK, then the sign where the SIGN clause puts it.
135fn store_zoned(v: i128, bytes: &mut [u8], digits: u32, signed: bool, sign: Option<SignClause>) {
136    let negative = v < 0;
137    let mut packed = vec![0u8; digits as usize / 2 + 1];
138    decimal::encode(&mut packed, Decimal { negative, magnitude: v.unsigned_abs() }).expect("the packed image holds the item's digits");
139    let unpack = |zoned: &mut [u8]| decimal::unpk(zoned, &packed).expect("a zoned host variable is 1 to 16 bytes");
140    match sign {
141        Some(SignClause { separate: true, position }) => {
142            let n = bytes.len();
143            let (sign_at, digits_at) = if position == SignPosition::Leading { (0, 1..n) } else { (n - 1, 0..n - 1) };
144            unpack(&mut bytes[digits_at.clone()]);
145            bytes[digits_at.end - 1] |= 0xF0;
146            bytes[sign_at] = if negative { 0x60 } else { 0x4E };
147        }
148        Some(SignClause { separate: false, position: SignPosition::Leading }) => {
149            unpack(bytes);
150            let last = bytes.len() - 1;
151            let zone = bytes[last] & 0xF0;
152            bytes[last] |= 0xF0;
153            bytes[0] = zone | (bytes[0] & 0x0F);
154        }
155        _ => {
156            unpack(bytes);
157            if !signed {
158                *bytes.last_mut().unwrap() |= 0xF0;
159            }
160        }
161    }
162}
163
164fn float(value: &Value) -> Result<f64, SqlError> {
165    match *value {
166        Value::Int(v) => Ok(v as f64),
167        Value::Decimal { value, scale } => Ok(value as f64 / 10f64.powi(scale as i32)),
168        Value::Double(f) => Ok(f),
169        _ => Err(NOT_ASSIGNABLE),
170    }
171}
172
173/// An IEEE double as a hexadecimal floating-point storage image, low-order bits that do not fit
174/// the short form dropped (assumption SQ9). None when the exponent is outside HFP's range.
175fn hfp_image(f: f64, precision: Precision) -> Option<Vec<u8>> {
176    let len = precision.bytes();
177    if f == 0.0 {
178        return Some(vec![0; len]);
179    }
180    if !f.is_finite() {
181        return None;
182    }
183    let bits = f.to_bits();
184    let raw_exponent = ((bits >> 52) & 0x7FF) as i32;
185    let (mantissa, exponent) = match raw_exponent {
186        0 => (bits & ((1 << 52) - 1), -1074),
187        e => ((bits & ((1 << 52) - 1)) | 1 << 52, e - 1075),
188    };
189    let top = exponent + (64 - mantissa.leading_zeros() as i32);
190    let hex = top.div_euclid(4) + i32::from(top.rem_euclid(4) != 0);
191    let characteristic = hex + 64;
192    if !(0..=127).contains(&characteristic) {
193        return None;
194    }
195    let fraction_bits = if precision == Precision::Short { 24 } else { 56 };
196    let shift = exponent + fraction_bits - 4 * hex;
197    let fraction = if shift >= 0 { mantissa << shift } else { mantissa >> -shift };
198    let lead = u64::from(f.is_sign_negative()) << 7 | characteristic as u64;
199    Some(match precision {
200        Precision::Short => ((lead << 24 | fraction) as u32).to_be_bytes().to_vec(),
201        _ => (lead << 56 | fraction).to_be_bytes().to_vec(),
202    })
203}
204
205fn text(value: &Value, page: &CodePage) -> Result<Vec<u8>, SqlError> {
206    match value {
207        Value::Char(s) => page.encode(s).map_err(|_| UNCONVERTIBLE),
208        Value::Binary(b) => Ok(b.clone()),
209        _ => Err(NOT_ASSIGNABLE),
210    }
211}
212
213/// A fixed-length string: padded with the code page's space, or cut and reported.
214fn store_text(t: &[u8], bytes: &mut [u8], page: &CodePage) -> Result<Written, SqlError> {
215    let n = t.len().min(bytes.len());
216    bytes[..n].copy_from_slice(&t[..n]);
217    bytes[n..].fill(page.encode_char(' ').unwrap_or(0x40));
218    Ok(Written { truncated_from: (t.len() > bytes.len()).then_some(t.len()) })
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    fn page() -> &'static CodePage {
226        CodePage::by_ccsid(37).expect("CCSID 37 is carried")
227    }
228
229    fn get(bytes: &[u8], ty: &HostType) -> Result<Value, ReadError> {
230        read(bytes, ty, page(), Numproc::Nopfd)
231    }
232
233    fn put(value: Value, len: usize, ty: &HostType) -> (Vec<u8>, Result<Written, SqlError>) {
234        let mut bytes = vec![0xEE; len];
235        let r = write(&value, &mut bytes, ty, page());
236        (bytes, r)
237    }
238
239    const S5V2_PACKED: HostType = HostType::Decimal { digits: 7, scale: 2, signed: true };
240
241    #[test]
242    fn binary_host_variables() {
243        assert_eq!(get(&[0xFF, 0xFE], &HostType::SmallInt { signed: true }), Ok(Value::Int(-2)));
244        assert_eq!(get(&[0xFF, 0xFE], &HostType::SmallInt { signed: false }), Ok(Value::Int(65534)));
245        assert_eq!(get(&[0, 0, 1, 0], &HostType::Integer { signed: true }), Ok(Value::Int(256)));
246        assert_eq!(put(Value::Int(-2), 2, &HostType::SmallInt { signed: true }).0, [0xFF, 0xFE]);
247        assert_eq!(put(Value::Int(40_000), 2, &HostType::SmallInt { signed: true }).1, Err(OUT_OF_RANGE));
248        assert_eq!(put(Value::Int(-1), 4, &HostType::Integer { signed: false }).1, Err(OUT_OF_RANGE));
249        assert_eq!(put(Value::Decimal { value: 12_399, scale: 2 }, 4, &HostType::Integer { signed: true }).0, [0, 0, 0, 123]);
250    }
251
252    #[test]
253    fn packed_host_variables() {
254        assert_eq!(get(&[0x01, 0x23, 0x45, 0x0D], &S5V2_PACKED), Ok(Value::Decimal { value: -123_450, scale: 2 }));
255        assert_eq!(get(&[0x01, 0x23, 0x45, 0x0A << 4], &S5V2_PACKED), Err(ReadError::Check(ProgramCheck::Data)));
256        assert_eq!(put(Value::Decimal { value: -123_450, scale: 2 }, 4, &S5V2_PACKED).0, [0x01, 0x23, 0x45, 0x0D]);
257        assert_eq!(put(Value::Decimal { value: 1_239, scale: 3 }, 4, &S5V2_PACKED).0, [0x00, 0x00, 0x12, 0x3C]);
258        assert_eq!(put(Value::Decimal { value: 12_345_600, scale: 2 }, 4, &S5V2_PACKED).1, Err(OUT_OF_RANGE));
259        assert_eq!(put(Value::Int(7), 2, &HostType::Decimal { digits: 3, scale: 0, signed: false }).0, [0x00, 0x7F]);
260    }
261
262    #[test]
263    fn zoned_host_variables() {
264        let trailing = HostType::Zoned { digits: 5, scale: 0, signed: true, sign: None };
265        assert_eq!(put(Value::Int(-123), 5, &trailing).0, [0xF0, 0xF0, 0xF1, 0xF2, 0xD3]);
266        assert_eq!(get(&[0xF0, 0xF0, 0xF1, 0xF2, 0xD3], &trailing), Ok(Value::Decimal { value: -123, scale: 0 }));
267        let leading_separate = HostType::Zoned { digits: 5, scale: 0, signed: true, sign: Some(SignClause { position: SignPosition::Leading, separate: true }) };
268        assert_eq!(put(Value::Int(-123), 6, &leading_separate).0, [0x60, 0xF0, 0xF0, 0xF1, 0xF2, 0xF3]);
269        assert_eq!(get(&[0x60, 0xF0, 0xF0, 0xF1, 0xF2, 0xF3], &leading_separate), Ok(Value::Decimal { value: -123, scale: 0 }));
270        let leading = HostType::Zoned { digits: 3, scale: 0, signed: true, sign: Some(SignClause { position: SignPosition::Leading, separate: false }) };
271        assert_eq!(put(Value::Int(45), 3, &leading).0, [0xC0, 0xF4, 0xF5]);
272    }
273
274    #[test]
275    fn strings_pad_cut_and_report() {
276        let (bytes, r) = put(Value::Char("ABCDEFGHIJ".into()), 5, &HostType::Char(5));
277        assert_eq!((page().decode(&bytes), r), ("ABCDE".into(), Ok(Written { truncated_from: Some(10) })));
278        let (bytes, r) = put(Value::Char("AB".into()), 5, &HostType::Char(5));
279        assert_eq!((page().decode(&bytes), r), ("AB   ".into(), Ok(Written::default())));
280        let (bytes, r) = put(Value::Char("HELLO".into()), 5, &HostType::VarChar(3));
281        assert_eq!((bytes[..2].to_vec(), page().decode(&bytes[2..]), r), (vec![0, 3], "HEL".into(), Ok(Written { truncated_from: Some(5) })));
282        let mut varchar = vec![0, 2];
283        varchar.extend(page().encode("OKXX").unwrap());
284        assert_eq!(get(&varchar, &HostType::VarChar(4)), Ok(Value::Char("OK".into())));
285        assert_eq!(get(&[0, 9, 0xC1, 0xC1], &HostType::VarChar(2)), Err(ReadError::Sql(BAD_LENGTH)));
286        assert_eq!(put(Value::Int(1), 5, &HostType::Char(5)).1, Err(NOT_ASSIGNABLE));
287        assert_eq!(put(Value::Char("1".into()), 2, &HostType::SmallInt { signed: true }).1, Err(NOT_ASSIGNABLE));
288    }
289
290    #[test]
291    fn floats_cross_between_ieee_and_hfp() {
292        assert_eq!(put(Value::Double(1.0), 8, &HostType::Double).0, [0x41, 0x10, 0, 0, 0, 0, 0, 0]);
293        assert_eq!(put(Value::Double(-2.5), 8, &HostType::Double).0, [0xC1, 0x28, 0, 0, 0, 0, 0, 0]);
294        assert_eq!(put(Value::Double(0.0), 4, &HostType::Real).0, [0, 0, 0, 0]);
295        assert_eq!(put(Value::Double(0.5), 4, &HostType::Real).0, [0x40, 0x80, 0, 0]);
296        for f in [1.0, -2.5, 0.1, 1234.5678, 1e-20, 6.02e23] {
297            let (bytes, _) = put(Value::Double(f), 8, &HostType::Double);
298            let Ok(Value::Double(back)) = get(&bytes, &HostType::Double) else { panic!() };
299            assert!((back - f).abs() <= f.abs() * 1e-15, "{f} came back as {back}");
300        }
301        assert_eq!(put(Value::Double(1e300), 8, &HostType::Double).1, Err(OUT_OF_RANGE));
302    }
303}