1use 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::{out_of_range, time::timestamp_plus_interval};
20
21const NANOS_PER_MICROSECOND: i64 = 1_000;
22const NANOS_PER_MILLISECOND: i64 = 1_000_000;
23const UUID_V1_UNIX_EPOCH_OFFSET_TICKS: i128 = 0x01b2_1dd2_1381_4000;
24const UUID_V7_SUBMILLISECOND_BITS: u32 = 12;
25const UUID_V7_MAX_UNIX_MILLISECONDS: i64 = 0x0000_ffff_ffff_ffff;
26
27#[cfg(any(target_os = "macos", target_os = "windows"))]
28const UUID_V7_CLOCK_PRECISION_BITS: u32 = 10;
29#[cfg(not(any(target_os = "macos", target_os = "windows")))]
30const UUID_V7_CLOCK_PRECISION_BITS: u32 = 12;
31
32const UUID_V7_MINIMUM_STEP_NANOS: i64 =
33 NANOS_PER_MILLISECOND / (1_i64 << UUID_V7_CLOCK_PRECISION_BITS) + 1;
34static UUID_V7_PREVIOUS_NANOS: AtomicI64 = AtomicI64::new(0);
35
36pub(super) fn canonicalize_uuid_with_control(
37 text: &str,
38 control: &ProductionControl<'_>,
39) -> Result<Produced<String>> {
40 let bytes = parse_uuid_bytes_with_control(text, control)?;
41 format_uuid_with_control(bytes, control)
42}
43
44pub(super) fn extract_uuid_version(
45 value: &Value,
46 control: &ProductionControl<'_>,
47) -> Result<Value> {
48 let bytes = uuid_value_bytes(value, control)?;
49 Ok(uuid_version(&bytes).map_or(Value::Null, |version| Value::Int(i64::from(version))))
50}
51
52pub(super) fn extract_uuid_timestamp(
53 value: &Value,
54 control: &ProductionControl<'_>,
55) -> Result<Value> {
56 let bytes = uuid_value_bytes(value, control)?;
57 let Some(version) = uuid_version(&bytes) else {
58 return Ok(Value::Null);
59 };
60 let micros = match version {
61 1 => uuid_v1_unix_micros(&bytes),
62 7 => uuid_v7_unix_micros(&bytes),
63 _ => return Ok(Value::Null),
64 }?;
65 Ok(Value::Temporal(TemporalValue::TimestampTz { micros }))
66}
67
68pub(super) fn generate_random_uuid() -> Result<String> {
69 let mut bytes = [0u8; 16];
70 getrandom::fill(&mut bytes)
71 .map_err(|error| SQLError::Internal(format!("failed to obtain random bytes: {error}")))?;
72 bytes[6] = (bytes[6] & 0x0f) | 0x40;
73 bytes[8] = (bytes[8] & 0x3f) | 0x80;
74 Ok(format_uuid(bytes))
75}
76
77pub(super) fn generate_uuid_v7(shift: Option<&TemporalValue>) -> Result<String> {
78 let now_nanos = real_time_nanos_ascending()?;
79 let now_micros = now_nanos.div_euclid(NANOS_PER_MICROSECOND);
80 let sub_microsecond_nanos = now_nanos.rem_euclid(NANOS_PER_MICROSECOND);
81 let timestamp_micros = match shift {
82 None => now_micros,
83 Some(TemporalValue::Interval {
84 months,
85 days,
86 micros,
87 }) => timestamp_plus_interval(now_micros, *months, *days, *micros)?,
88 Some(other) => {
89 return Err(SQLError::TypeMismatch(format!(
90 "uuidv7: expected interval, got {other:?}"
91 )));
92 }
93 };
94 let unix_millis = timestamp_micros.div_euclid(1_000);
95 if !(0..=UUID_V7_MAX_UNIX_MILLISECONDS).contains(&unix_millis) {
96 return Err(out_of_range("uuidv7 timestamp"));
97 }
98 let sub_millisecond_nanos = timestamp_micros
99 .rem_euclid(1_000)
100 .checked_mul(NANOS_PER_MICROSECOND)
101 .and_then(|nanos| nanos.checked_add(sub_microsecond_nanos))
102 .ok_or_else(|| out_of_range("uuidv7 timestamp"))?;
103 let sub_millisecond_nanos =
104 u32::try_from(sub_millisecond_nanos).map_err(|_| out_of_range("uuidv7 timestamp"))?;
105 generate_uuid_v7_at(unix_millis as u64, sub_millisecond_nanos)
106}
107
108pub fn parse_uuid_bytes(text: &str) -> Result<[u8; 16]> {
110 parse_uuid_bytes_with_control(text, &ProductionControl::uncontrolled())
111}
112
113fn parse_uuid_bytes_with_control(text: &str, control: &ProductionControl<'_>) -> Result<[u8; 16]> {
114 control.check()?;
115 let digits = text
116 .strip_prefix('{')
117 .and_then(|text| text.strip_suffix('}'))
118 .unwrap_or(text);
119 if digits.starts_with('{') || digits.ends_with('}') {
120 return Err(invalid_uuid(text));
121 }
122 let mut normalized = [0_u8; 32];
123 let mut digit_count = 0;
124 let mut group_digits = 0_usize;
125 for character in digits.chars() {
126 control.check()?;
127 if character == '-' {
128 if group_digits == 0 || !group_digits.is_multiple_of(4) {
129 return Err(invalid_uuid(text));
130 }
131 group_digits = 0;
132 continue;
133 }
134 if !character.is_ascii_hexdigit() {
135 return Err(invalid_uuid(text));
136 }
137 let Some(digit) = normalized.get_mut(digit_count) else {
138 return Err(invalid_uuid(text));
139 };
140 *digit = character.to_ascii_lowercase() as u8;
141 digit_count += 1;
142 group_digits += 1;
143 }
144 if digit_count != 32 || group_digits == 0 {
145 return Err(invalid_uuid(text));
146 }
147 let mut bytes = [0_u8; 16];
148 for (index, pair) in normalized.chunks_exact(2).enumerate() {
149 bytes[index] = (hex_value(pair[0]) << 4) | hex_value(pair[1]);
150 }
151 Ok(bytes)
152}
153
154fn uuid_value_bytes(value: &Value, control: &ProductionControl<'_>) -> Result<[u8; 16]> {
155 match value {
156 Value::Str(text) | Value::FixedChar(text) => parse_uuid_bytes_with_control(text, control),
157 other => Err(SQLError::TypeMismatch(format!(
158 "expected uuid value, got {other:?}"
159 ))),
160 }
161}
162
163fn uuid_version(bytes: &[u8; 16]) -> Option<u8> {
164 ((bytes[8] & 0xc0) == 0x80).then_some(bytes[6] >> 4)
165}
166
167fn uuid_v1_unix_micros(bytes: &[u8; 16]) -> Result<i64> {
168 let low = u32::from_be_bytes(bytes[0..4].try_into().expect("UUID time_low width"));
169 let middle = u16::from_be_bytes(bytes[4..6].try_into().expect("UUID time_mid width"));
170 let high = u16::from_be_bytes(bytes[6..8].try_into().expect("UUID time_high width")) & 0x0fff;
171 let ticks = (i128::from(high) << 48) | (i128::from(middle) << 32) | i128::from(low);
172 i64::try_from((ticks - UUID_V1_UNIX_EPOCH_OFFSET_TICKS).div_euclid(10))
173 .map_err(|_| out_of_range("uuid timestamp"))
174}
175
176fn uuid_v7_unix_micros(bytes: &[u8; 16]) -> Result<i64> {
177 let milliseconds = bytes[..6]
178 .iter()
179 .fold(0_i64, |value, byte| (value << 8) | i64::from(*byte));
180 milliseconds
181 .checked_mul(1_000)
182 .ok_or_else(|| out_of_range("uuid timestamp"))
183}
184
185fn real_time_nanos_ascending() -> Result<i64> {
186 let elapsed = SystemTime::now()
187 .duration_since(UNIX_EPOCH)
188 .map_err(|_| out_of_range("uuidv7 timestamp"))?;
189 let actual = i64::try_from(elapsed.as_nanos()).map_err(|_| out_of_range("uuidv7 timestamp"))?;
190 loop {
191 let previous = UUID_V7_PREVIOUS_NANOS.load(AtomicOrdering::Relaxed);
192 let minimum = previous
193 .checked_add(UUID_V7_MINIMUM_STEP_NANOS)
194 .ok_or_else(|| out_of_range("uuidv7 timestamp"))?;
195 let candidate = if minimum >= actual { minimum } else { actual };
196 if UUID_V7_PREVIOUS_NANOS
197 .compare_exchange_weak(
198 previous,
199 candidate,
200 AtomicOrdering::Relaxed,
201 AtomicOrdering::Relaxed,
202 )
203 .is_ok()
204 {
205 return Ok(candidate);
206 }
207 }
208}
209
210fn generate_uuid_v7_at(unix_millis: u64, sub_millisecond_nanos: u32) -> Result<String> {
211 if unix_millis > UUID_V7_MAX_UNIX_MILLISECONDS as u64
212 || sub_millisecond_nanos >= NANOS_PER_MILLISECOND as u32
213 {
214 return Err(out_of_range("uuidv7 timestamp"));
215 }
216 let mut bytes = [0u8; 16];
217 getrandom::fill(&mut bytes[8..])
218 .map_err(|error| SQLError::Internal(format!("failed to obtain random bytes: {error}")))?;
219 let timestamp = unix_millis.to_be_bytes();
220 bytes[..6].copy_from_slice(×tamp[2..]);
221 let increased_clock_precision = (u64::from(sub_millisecond_nanos)
222 * (1_u64 << UUID_V7_SUBMILLISECOND_BITS))
223 / NANOS_PER_MILLISECOND as u64;
224 bytes[6] = (increased_clock_precision >> 8) as u8;
225 bytes[7] = increased_clock_precision as u8;
226
227 #[cfg(any(target_os = "macos", target_os = "windows"))]
228 {
229 bytes[7] ^= bytes[8] >> 6;
230 }
231
232 bytes[6] = (bytes[6] & 0x0f) | 0x70;
233 bytes[8] = (bytes[8] & 0x3f) | 0x80;
234 Ok(format_uuid(bytes))
235}
236
237fn format_uuid(bytes: [u8; 16]) -> String {
238 format_uuid_with_control(bytes, &ProductionControl::uncontrolled())
239 .expect("ordinary UUID formatting")
240 .into_uncontrolled()
241 .expect("ordinary UUID text")
242}
243
244fn format_uuid_with_control(
245 bytes: [u8; 16],
246 control: &ProductionControl<'_>,
247) -> Result<Produced<String>> {
248 Ok(control.format(format_args!(
249 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
250 bytes[0], bytes[1], bytes[2], bytes[3],
251 bytes[4], bytes[5],
252 bytes[6], bytes[7],
253 bytes[8], bytes[9],
254 bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
255 ))?)
256}
257
258fn hex_value(byte: u8) -> u8 {
259 match byte {
260 b'0'..=b'9' => byte - b'0',
261 b'a'..=b'f' => byte - b'a' + 10,
262 _ => unreachable!("UUID parser retained only lowercase hexadecimal digits"),
263 }
264}
265
266fn invalid_uuid(text: &str) -> SQLError {
267 SQLError::Routine {
268 sqlstate: "22P02".into(),
269 message: format!("invalid input syntax for type uuid: \"{text}\""),
270 }
271}