Skip to main content

uqa_sql/expr/
uuid.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! PostgreSQL-compatible UUID parsing, generation, and extraction.
8
9use std::sync::atomic::{AtomicI64, Ordering as AtomicOrdering};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use uqa_core::{
13    memory::{Produced, ProductionControl},
14    TemporalValue, Value,
15};
16
17use crate::error::{Result, SQLError};
18
19use super::{
20    out_of_range, time::timestamp_tz_plus_interval, EngineHook, EvalContext, IntervalFields,
21};
22
23const NANOS_PER_MICROSECOND: i64 = 1_000;
24const NANOS_PER_MILLISECOND: i64 = 1_000_000;
25const UUID_V1_UNIX_EPOCH_OFFSET_TICKS: i128 = 0x01b2_1dd2_1381_4000;
26const UUID_V7_SUBMILLISECOND_BITS: u32 = 12;
27
28#[cfg(any(target_os = "macos", target_os = "windows"))]
29const UUID_V7_CLOCK_PRECISION_BITS: u32 = 10;
30#[cfg(not(any(target_os = "macos", target_os = "windows")))]
31const UUID_V7_CLOCK_PRECISION_BITS: u32 = 12;
32
33const UUID_V7_MINIMUM_STEP_NANOS: i64 =
34    NANOS_PER_MILLISECOND / (1_i64 << UUID_V7_CLOCK_PRECISION_BITS) + 1;
35static UUID_V7_PREVIOUS_NANOS: AtomicI64 = AtomicI64::new(0);
36
37pub(super) fn canonicalize_uuid_with_control(
38    text: &str,
39    control: &ProductionControl<'_>,
40) -> Result<Produced<String>> {
41    let bytes = parse_uuid_bytes_with_control(text, control)?;
42    format_uuid_with_control(bytes, control)
43}
44
45pub(super) fn extract_uuid_version(
46    value: &Value,
47    control: &ProductionControl<'_>,
48) -> Result<Value> {
49    let bytes = uuid_value_bytes(value, control)?;
50    Ok(uuid_version(&bytes).map_or(Value::Null, |version| Value::Int(i64::from(version))))
51}
52
53pub(super) fn extract_uuid_timestamp(
54    value: &Value,
55    control: &ProductionControl<'_>,
56) -> Result<Value> {
57    let bytes = uuid_value_bytes(value, control)?;
58    let Some(version) = uuid_version(&bytes) else {
59        return Ok(Value::Null);
60    };
61    let micros = match version {
62        1 => uuid_v1_unix_micros(&bytes),
63        7 => uuid_v7_unix_micros(&bytes),
64        _ => return Ok(Value::Null),
65    }?;
66    Ok(Value::Temporal(TemporalValue::TimestampTz { micros }))
67}
68
69pub(super) fn generate_random_uuid() -> Result<String> {
70    let mut bytes = [0u8; 16];
71    getrandom::fill(&mut bytes)
72        .map_err(|error| SQLError::Internal(format!("failed to obtain random bytes: {error}")))?;
73    bytes[6] = (bytes[6] & 0x0f) | 0x40;
74    bytes[8] = (bytes[8] & 0x3f) | 0x80;
75    Ok(format_uuid(bytes))
76}
77
78pub(super) fn eval_uuid_v7(args: &[Value], context: Option<&EvalContext<'_>>) -> Result<Value> {
79    let shift = match args {
80        [] => None,
81        [Value::Temporal(interval @ TemporalValue::Interval { .. })] => Some(interval),
82        [Value::Null] => return Ok(Value::Null),
83        [_] => {
84            return Err(SQLError::TypeMismatch(
85                "uuidv7 shift must be an interval".into(),
86            ))
87        }
88        _ => return Err(SQLError::TypeMismatch("uuidv7 takes 0 or 1 args".into())),
89    };
90    generate_uuid_v7_at_clock(
91        real_time_nanos_ascending()?,
92        shift,
93        context.and_then(|context| context.engine),
94    )
95    .map(Value::Str)
96}
97
98fn generate_uuid_v7_at_clock(
99    now_nanos: i64,
100    shift: Option<&TemporalValue>,
101    engine: Option<&dyn EngineHook>,
102) -> Result<String> {
103    let now_micros = now_nanos.div_euclid(NANOS_PER_MICROSECOND);
104    let sub_microsecond_nanos = now_nanos.rem_euclid(NANOS_PER_MICROSECOND);
105    let timestamp_micros = match shift {
106        None => now_micros,
107        Some(TemporalValue::Interval {
108            months,
109            days,
110            micros,
111        }) => timestamp_tz_plus_interval(
112            now_micros,
113            IntervalFields {
114                months: *months,
115                days: *days,
116                micros: *micros,
117            },
118            engine,
119            &ProductionControl::uncontrolled(),
120        )?,
121        Some(other) => {
122            return Err(SQLError::TypeMismatch(format!(
123                "uuidv7: expected interval, got {other:?}"
124            )));
125        }
126    };
127    let timestamp = TemporalValue::TimestampTz {
128        micros: timestamp_micros,
129    };
130    // uuidv7_interval adds the Unix epoch to PostgreSQL's raw timestamp after
131    // arithmetic, including its infinity bit patterns, with signed wrapping.
132    let unix_micros = match timestamp.infinity() {
133        Some(uqa_core::TemporalInfinity::Positive) => i64::MAX.wrapping_add(946_684_800_000_000),
134        Some(uqa_core::TemporalInfinity::Negative) => i64::MIN.wrapping_add(946_684_800_000_000),
135        None => timestamp_micros,
136    };
137    // PostgreSQL truncates signed division toward zero before narrowing the
138    // millisecond and fractional parts to the generator's unsigned arguments.
139    let unix_millis = (unix_micros / 1_000).cast_unsigned();
140    let sub_millisecond_nanos =
141        (unix_micros % 1_000) * NANOS_PER_MICROSECOND + sub_microsecond_nanos;
142    #[expect(
143        clippy::cast_possible_truncation,
144        clippy::cast_sign_loss,
145        reason = "PostgreSQL narrows the signed sub-millisecond expression to uint32"
146    )]
147    generate_uuid_v7_at(unix_millis, sub_millisecond_nanos as u32)
148}
149
150/// Parse every UUID input spelling accepted by `PostgreSQL` into network-order bytes.
151pub fn parse_uuid_bytes(text: &str) -> Result<[u8; 16]> {
152    parse_uuid_bytes_with_control(text, &ProductionControl::uncontrolled())
153}
154
155fn parse_uuid_bytes_with_control(text: &str, control: &ProductionControl<'_>) -> Result<[u8; 16]> {
156    control.check()?;
157    let digits = text
158        .strip_prefix('{')
159        .and_then(|text| text.strip_suffix('}'))
160        .unwrap_or(text);
161    if digits.starts_with('{') || digits.ends_with('}') {
162        return Err(invalid_uuid(text));
163    }
164    let mut normalized = [0_u8; 32];
165    let mut digit_count = 0;
166    let mut group_digits = 0_usize;
167    for character in digits.chars() {
168        control.check()?;
169        if character == '-' {
170            if group_digits == 0 || !group_digits.is_multiple_of(4) {
171                return Err(invalid_uuid(text));
172            }
173            group_digits = 0;
174            continue;
175        }
176        if !character.is_ascii_hexdigit() {
177            return Err(invalid_uuid(text));
178        }
179        let Some(digit) = normalized.get_mut(digit_count) else {
180            return Err(invalid_uuid(text));
181        };
182        *digit = character.to_ascii_lowercase() as u8;
183        digit_count += 1;
184        group_digits += 1;
185    }
186    if digit_count != 32 || group_digits == 0 {
187        return Err(invalid_uuid(text));
188    }
189    let mut bytes = [0_u8; 16];
190    for (index, pair) in normalized.as_chunks::<2>().0.iter().enumerate() {
191        bytes[index] = (hex_value(pair[0]) << 4) | hex_value(pair[1]);
192    }
193    Ok(bytes)
194}
195
196fn uuid_value_bytes(value: &Value, control: &ProductionControl<'_>) -> Result<[u8; 16]> {
197    match value {
198        Value::Str(text) | Value::FixedChar(text) => parse_uuid_bytes_with_control(text, control),
199        other => Err(SQLError::TypeMismatch(format!(
200            "expected uuid value, got {other:?}"
201        ))),
202    }
203}
204
205fn uuid_version(bytes: &[u8; 16]) -> Option<u8> {
206    ((bytes[8] & 0xc0) == 0x80).then_some(bytes[6] >> 4)
207}
208
209fn uuid_v1_unix_micros(bytes: &[u8; 16]) -> Result<i64> {
210    let low = u32::from_be_bytes(bytes[0..4].try_into().expect("UUID time_low width"));
211    let middle = u16::from_be_bytes(bytes[4..6].try_into().expect("UUID time_mid width"));
212    let high = u16::from_be_bytes(bytes[6..8].try_into().expect("UUID time_high width")) & 0x0fff;
213    let ticks = (i128::from(high) << 48) | (i128::from(middle) << 32) | i128::from(low);
214    i64::try_from((ticks - UUID_V1_UNIX_EPOCH_OFFSET_TICKS).div_euclid(10))
215        .map_err(|_| out_of_range("uuid timestamp"))
216}
217
218fn uuid_v7_unix_micros(bytes: &[u8; 16]) -> Result<i64> {
219    let milliseconds = bytes[..6]
220        .iter()
221        .fold(0_i64, |value, byte| (value << 8) | i64::from(*byte));
222    milliseconds
223        .checked_mul(1_000)
224        .ok_or_else(|| out_of_range("uuid timestamp"))
225}
226
227fn real_time_nanos_ascending() -> Result<i64> {
228    let elapsed = SystemTime::now()
229        .duration_since(UNIX_EPOCH)
230        .map_err(|_| out_of_range("uuidv7 timestamp"))?;
231    let actual = i64::try_from(elapsed.as_nanos()).map_err(|_| out_of_range("uuidv7 timestamp"))?;
232    loop {
233        let previous = UUID_V7_PREVIOUS_NANOS.load(AtomicOrdering::Relaxed);
234        let minimum = previous
235            .checked_add(UUID_V7_MINIMUM_STEP_NANOS)
236            .ok_or_else(|| out_of_range("uuidv7 timestamp"))?;
237        let candidate = if minimum >= actual { minimum } else { actual };
238        if UUID_V7_PREVIOUS_NANOS
239            .compare_exchange_weak(
240                previous,
241                candidate,
242                AtomicOrdering::Relaxed,
243                AtomicOrdering::Relaxed,
244            )
245            .is_ok()
246        {
247            return Ok(candidate);
248        }
249    }
250}
251
252fn generate_uuid_v7_at(unix_millis: u64, sub_millisecond_nanos: u32) -> Result<String> {
253    let mut bytes = [0u8; 16];
254    getrandom::fill(&mut bytes[8..])
255        .map_err(|error| SQLError::Internal(format!("failed to obtain random bytes: {error}")))?;
256    let timestamp = unix_millis.to_be_bytes();
257    bytes[..6].copy_from_slice(&timestamp[2..]);
258    let increased_clock_precision = sub_millisecond_nanos
259        .wrapping_mul(1_u32 << UUID_V7_SUBMILLISECOND_BITS)
260        / NANOS_PER_MILLISECOND as u32;
261    bytes[6] = (increased_clock_precision >> 8) as u8;
262    bytes[7] = increased_clock_precision as u8;
263
264    #[cfg(any(target_os = "macos", target_os = "windows"))]
265    {
266        bytes[7] ^= bytes[8] >> 6;
267    }
268
269    bytes[6] = (bytes[6] & 0x0f) | 0x70;
270    bytes[8] = (bytes[8] & 0x3f) | 0x80;
271    Ok(format_uuid(bytes))
272}
273
274pub(crate) fn format_uuid(bytes: [u8; 16]) -> String {
275    format_uuid_with_control(bytes, &ProductionControl::uncontrolled())
276        .expect("ordinary UUID formatting")
277        .into_uncontrolled()
278        .expect("ordinary UUID text")
279}
280
281pub(super) fn format_uuid_with_control(
282    bytes: [u8; 16],
283    control: &ProductionControl<'_>,
284) -> Result<Produced<String>> {
285    Ok(control.format(format_args!(
286        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
287        bytes[0], bytes[1], bytes[2], bytes[3],
288        bytes[4], bytes[5],
289        bytes[6], bytes[7],
290        bytes[8], bytes[9],
291        bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
292    ))?)
293}
294
295fn hex_value(byte: u8) -> u8 {
296    match byte {
297        b'0'..=b'9' => byte - b'0',
298        b'a'..=b'f' => byte - b'a' + 10,
299        _ => unreachable!("UUID parser retained only lowercase hexadecimal digits"),
300    }
301}
302
303fn invalid_uuid(text: &str) -> SQLError {
304    SQLError::Routine {
305        sqlstate: "22P02".into(),
306        message: format!("invalid input syntax for type uuid: \"{text}\""),
307    }
308}
309
310#[cfg(test)]
311mod tests;