Skip to main content

uqa_sql/semantics/parameters/
units.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Integer parameter values and their units, read and shown as `PostgreSQL`'s `parse_int`, `convert_to_base_unit` and `convert_int_from_base_unit` do: C `strtol` and `strtod` number syntax, a unit of up to three characters, rounding to the nearest multiple of the next smaller unit, and display in the greatest unit that divides the value.
8
9/// The base unit of an integer parameter (`GUC_UNIT_*`).
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum ParameterUnit {
12    Bytes,
13    Kilobytes,
14    Megabytes,
15    Milliseconds,
16    Seconds,
17    Minutes,
18}
19
20const MEMORY_UNITS_HINT: &str =
21    "Valid units for this parameter are \"B\", \"kB\", \"MB\", \"GB\", and \"TB\".";
22const TIME_UNITS_HINT: &str =
23    "Valid units for this parameter are \"us\", \"ms\", \"s\", \"min\", \"h\", and \"d\".";
24const INTEGER_RANGE_HINT: &str = "Value exceeds integer range.";
25const MAX_UNIT_LENGTH: usize = 3;
26const KIB: f64 = 1024.0;
27
28impl ParameterUnit {
29    /// The name that `pg_settings.unit` and range errors report.
30    pub const fn name(self) -> &'static str {
31        match self {
32            Self::Bytes => "B",
33            Self::Kilobytes => "kB",
34            Self::Megabytes => "MB",
35            Self::Milliseconds => "ms",
36            Self::Seconds => "s",
37            Self::Minutes => "min",
38        }
39    }
40
41    const fn hint(self) -> &'static str {
42        match self {
43            Self::Bytes | Self::Kilobytes | Self::Megabytes => MEMORY_UNITS_HINT,
44            Self::Milliseconds | Self::Seconds | Self::Minutes => TIME_UNITS_HINT,
45        }
46    }
47
48    /// The units a value may name and how many base units each holds, from the greatest to the smallest, as `memory_unit_conversion_table` and `time_unit_conversion_table` list them.
49    fn conversions(self) -> [(&'static str, f64); 6] {
50        match self {
51            Self::Bytes => [
52                ("TB", KIB * KIB * KIB * KIB),
53                ("GB", KIB * KIB * KIB),
54                ("MB", KIB * KIB),
55                ("kB", KIB),
56                ("B", 1.0),
57                ("", 0.0),
58            ],
59            Self::Kilobytes => [
60                ("TB", KIB * KIB * KIB),
61                ("GB", KIB * KIB),
62                ("MB", KIB),
63                ("kB", 1.0),
64                ("B", 1.0 / KIB),
65                ("", 0.0),
66            ],
67            Self::Megabytes => [
68                ("TB", KIB * KIB),
69                ("GB", KIB),
70                ("MB", 1.0),
71                ("kB", 1.0 / KIB),
72                ("B", 1.0 / (KIB * KIB)),
73                ("", 0.0),
74            ],
75            Self::Milliseconds => [
76                ("d", 86_400_000.0),
77                ("h", 3_600_000.0),
78                ("min", 60_000.0),
79                ("s", 1000.0),
80                ("ms", 1.0),
81                ("us", 1.0 / 1000.0),
82            ],
83            Self::Seconds => [
84                ("d", 86_400.0),
85                ("h", 3600.0),
86                ("min", 60.0),
87                ("s", 1.0),
88                ("ms", 1.0 / 1000.0),
89                ("us", 1.0 / (1000.0 * 1000.0)),
90            ],
91            Self::Minutes => [
92                ("d", 1440.0),
93                ("h", 60.0),
94                ("min", 1.0),
95                ("s", 1.0 / 60.0),
96                ("ms", 1.0 / (1000.0 * 60.0)),
97                ("us", 1.0 / (1000.0 * 1000.0 * 60.0)),
98            ],
99        }
100    }
101}
102
103/// Read an integer value in base units. The error carries the hint `PostgreSQL` adds to `invalid value for parameter`, if any.
104pub(super) fn parse_integer(
105    raw: &str,
106    unit: Option<ParameterUnit>,
107) -> Result<i32, Option<&'static str>> {
108    let bytes = raw.as_bytes();
109    let (integer, integer_end, overflow) = c_strtol(bytes);
110    let (mut value, mut end) = (integer as f64, integer_end);
111    if overflow || matches!(bytes.get(integer_end), Some(b'.' | b'e' | b'E')) {
112        let (double, double_end, out_of_range) = c_strtod(bytes);
113        if double_end == 0 || out_of_range {
114            return Err(None);
115        }
116        (value, end) = (double, double_end);
117    } else if integer_end == 0 {
118        return Err(None);
119    }
120    if value.is_nan() {
121        return Err(None);
122    }
123    end = skip_c_space(bytes, end);
124    if end < bytes.len() {
125        let unit = unit.ok_or(None)?;
126        value = convert_to_base_unit(value, &bytes[end..], unit).ok_or(Some(unit.hint()))?;
127    }
128    let value = value.round_ties_even();
129    if value > f64::from(i32::MAX) || value < f64::from(i32::MIN) {
130        return Err(Some(INTEGER_RANGE_HINT));
131    }
132    Ok(value as i32)
133}
134
135/// Show an integer value in the greatest unit that divides it.
136pub(super) fn show_integer(value: i64, unit: Option<ParameterUnit>) -> String {
137    let Some(unit) = unit.filter(|_| value > 0) else {
138        return value.to_string();
139    };
140    for (name, multiplier) in unit.conversions() {
141        if name.is_empty() {
142            break;
143        }
144        if multiplier <= 1.0 || value % (multiplier as i64) == 0 {
145            let converted = (value as f64 / multiplier).round_ties_even() as i64;
146            return format!("{converted}{name}");
147        }
148    }
149    unreachable!("every unit lists its base unit")
150}
151
152/// Convert `value` given in the unit that `text` names to base units, rounding a fraction to the nearest multiple of the next smaller unit; `None` for an unknown unit or text after it.
153fn convert_to_base_unit(value: f64, text: &[u8], unit: ParameterUnit) -> Option<f64> {
154    let length = text
155        .iter()
156        .take(MAX_UNIT_LENGTH)
157        .take_while(|byte| !is_c_space(**byte))
158        .count();
159    if skip_c_space(text, length) != text.len() {
160        return None;
161    }
162    let conversions = unit.conversions();
163    let index = conversions
164        .iter()
165        .position(|(name, _)| !name.is_empty() && name.as_bytes() == &text[..length])?;
166    let mut converted = value * conversions[index].1;
167    if let Some((next, multiplier)) = conversions.get(index + 1) {
168        if !next.is_empty() {
169            converted = (converted / multiplier).round_ties_even() * multiplier;
170        }
171    }
172    Some(converted)
173}
174
175/// The white space of the C locale's `isspace`.
176pub(super) fn is_c_space(byte: u8) -> bool {
177    matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
178}
179
180fn skip_c_space(bytes: &[u8], mut index: usize) -> usize {
181    while bytes.get(index).copied().is_some_and(is_c_space) {
182        index += 1;
183    }
184    index
185}
186
187/// C `strtol(text, &end, 0)` on a 64-bit `long`: the value, the offset where the conversion stopped (0 when nothing was converted) and whether the value overflowed. A leading `0x` selects hexadecimal and a leading `0` octal.
188fn c_strtol(bytes: &[u8]) -> (i64, usize, bool) {
189    let mut index = skip_c_space(bytes, 0);
190    let negative = match bytes.get(index) {
191        Some(b'-') => {
192            index += 1;
193            true
194        }
195        Some(b'+') => {
196            index += 1;
197            false
198        }
199        _ => false,
200    };
201    let hexadecimal = matches!(bytes.get(index..index + 2), Some(b"0x" | b"0X"))
202        && bytes.get(index + 2).is_some_and(u8::is_ascii_hexdigit);
203    let radix = if hexadecimal {
204        index += 2;
205        16
206    } else if bytes.get(index) == Some(&b'0') {
207        8
208    } else {
209        10
210    };
211    let start = index;
212    let mut magnitude: u128 = 0;
213    let mut overflow = false;
214    while let Some(digit) = bytes
215        .get(index)
216        .and_then(|byte| char::from(*byte).to_digit(radix))
217    {
218        magnitude = magnitude * u128::from(radix) + u128::from(digit);
219        if magnitude > u128::from(i64::MAX.unsigned_abs()) + 1 {
220            magnitude = u128::from(i64::MAX.unsigned_abs()) + 1;
221            overflow = true;
222        }
223        index += 1;
224    }
225    if index == start {
226        return (0, 0, false);
227    }
228    let value = if negative {
229        i64::try_from(-(magnitude as i128)).unwrap_or_else(|_| {
230            overflow = true;
231            i64::MIN
232        })
233    } else {
234        i64::try_from(magnitude).unwrap_or_else(|_| {
235            overflow = true;
236            i64::MAX
237        })
238    };
239    (value, index, overflow)
240}
241
242/// C `strtod`: the value, the offset where the conversion stopped (0 when nothing was converted) and whether the value overflowed or underflowed (`ERANGE`). Reads decimal and hexadecimal numbers, `inf`, `infinity` and `nan`.
243pub(super) fn c_strtod(bytes: &[u8]) -> (f64, usize, bool) {
244    let mut index = skip_c_space(bytes, 0);
245    let sign_start = index;
246    let negative = match bytes.get(index) {
247        Some(b'-') => {
248            index += 1;
249            true
250        }
251        Some(b'+') => {
252            index += 1;
253            false
254        }
255        _ => false,
256    };
257    let signed = |value: f64| if negative { -value } else { value };
258    let rest = &bytes[index..];
259    let starts_with =
260        |word: &[u8]| rest.len() >= word.len() && rest[..word.len()].eq_ignore_ascii_case(word);
261    if starts_with(b"infinity") {
262        return (signed(f64::INFINITY), index + 8, false);
263    }
264    if starts_with(b"inf") {
265        return (signed(f64::INFINITY), index + 3, false);
266    }
267    if starts_with(b"nan") {
268        let mut end = index + 3;
269        if bytes.get(end) == Some(&b'(') {
270            let close = bytes[end + 1..]
271                .iter()
272                .position(|byte| !(byte.is_ascii_alphanumeric() || *byte == b'_'));
273            if let Some(offset) = close {
274                if bytes[end + 1 + offset] == b')' {
275                    end += offset + 2;
276                }
277            }
278        }
279        return (f64::NAN, end, false);
280    }
281    if matches!(rest.get(..2), Some(b"0x" | b"0X")) {
282        if let Some((value, end, nonzero)) = hexadecimal_float(bytes, index + 2) {
283            return finish_strtod(signed(value), end, nonzero);
284        }
285    }
286    let mut end = index;
287    let mut nonzero = false;
288    let mut digits = 0;
289    while let Some(byte) = bytes.get(end).filter(|byte| byte.is_ascii_digit()) {
290        nonzero |= *byte != b'0';
291        digits += 1;
292        end += 1;
293    }
294    if bytes.get(end) == Some(&b'.') {
295        end += 1;
296        while let Some(byte) = bytes.get(end).filter(|byte| byte.is_ascii_digit()) {
297            nonzero |= *byte != b'0';
298            digits += 1;
299            end += 1;
300        }
301    }
302    if digits == 0 {
303        return (0.0, 0, false);
304    }
305    if matches!(bytes.get(end), Some(b'e' | b'E')) {
306        let mut exponent = end + 1;
307        if matches!(bytes.get(exponent), Some(b'+' | b'-')) {
308            exponent += 1;
309        }
310        if bytes.get(exponent).is_some_and(u8::is_ascii_digit) {
311            end = exponent;
312            while bytes.get(end).is_some_and(u8::is_ascii_digit) {
313                end += 1;
314            }
315        }
316    }
317    let text = std::str::from_utf8(&bytes[sign_start..end]).expect("ASCII number");
318    let value = text.parse::<f64>().expect("C decimal number syntax");
319    finish_strtod(value, end, nonzero)
320}
321
322fn finish_strtod(value: f64, end: usize, nonzero: bool) -> (f64, usize, bool) {
323    let out_of_range = value.is_infinite() || (nonzero && value.abs() < f64::MIN_POSITIVE);
324    (value, end, out_of_range)
325}
326
327/// The hexadecimal mantissa and optional binary exponent after `0x`: the magnitude, the offset after it and whether any digit was nonzero; `None` when no hexadecimal digit follows, which leaves `strtod` to read the `0` alone.
328fn hexadecimal_float(bytes: &[u8], start: usize) -> Option<(f64, usize, bool)> {
329    let mut index = start;
330    let mut mantissa: u64 = 0;
331    let mut exponent: i64 = 0;
332    let mut digits = 0;
333    let mut nonzero = false;
334    let mut accumulate = |digit: u32, fraction: bool, exponent: &mut i64| {
335        nonzero |= digit != 0;
336        if mantissa >> 60 == 0 {
337            mantissa = mantissa << 4 | u64::from(digit);
338            if fraction {
339                *exponent -= 4;
340            }
341        } else if !fraction {
342            *exponent += 4;
343        }
344    };
345    while let Some(digit) = bytes
346        .get(index)
347        .and_then(|byte| char::from(*byte).to_digit(16))
348    {
349        accumulate(digit, false, &mut exponent);
350        digits += 1;
351        index += 1;
352    }
353    if bytes.get(index) == Some(&b'.') {
354        let mut fraction = index + 1;
355        let mut fraction_digits = 0;
356        while let Some(digit) = bytes
357            .get(fraction)
358            .and_then(|byte| char::from(*byte).to_digit(16))
359        {
360            accumulate(digit, true, &mut exponent);
361            fraction_digits += 1;
362            fraction += 1;
363        }
364        if digits + fraction_digits > 0 {
365            index = fraction;
366            digits += fraction_digits;
367        }
368    }
369    if digits == 0 {
370        return None;
371    }
372    if matches!(bytes.get(index), Some(b'p' | b'P')) {
373        let mut cursor = index + 1;
374        let negative = match bytes.get(cursor) {
375            Some(b'-') => {
376                cursor += 1;
377                true
378            }
379            Some(b'+') => {
380                cursor += 1;
381                false
382            }
383            _ => false,
384        };
385        if bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
386            let mut binary: i64 = 0;
387            while let Some(byte) = bytes.get(cursor).filter(|byte| byte.is_ascii_digit()) {
388                binary = binary
389                    .saturating_mul(10)
390                    .saturating_add(i64::from(byte - b'0'));
391                cursor += 1;
392            }
393            exponent = exponent.saturating_add(if negative { -binary } else { binary });
394            index = cursor;
395        }
396    }
397    // Scale in steps that stay within the normal range, so that a large mantissa with a very negative exponent does not vanish before the multiplication.
398    let mut remaining = exponent.clamp(-2200, 2200);
399    let mut value = mantissa as f64;
400    while remaining > 1000 {
401        value *= 2f64.powi(1000);
402        remaining -= 1000;
403    }
404    while remaining < -1000 {
405        value *= 2f64.powi(-1000);
406        remaining += 1000;
407    }
408    let remaining = i32::try_from(remaining).expect("exponent within one step");
409    Some((value * 2f64.powi(remaining), index, nonzero))
410}
411
412#[cfg(test)]
413mod tests;