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::{
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 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 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
150pub 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(×tamp[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;