vrl 0.37.0

Vector Remap Language
Documentation
use crate::compiler::prelude::*;
use chrono::{DateTime, Utc};
use uuid::{NoContext, timestamp::Timestamp};

static DEFAULT_TIMESTAMP: Value = Value::from_static_str("`now()`");

const PARAMETERS: &[Parameter] = &[Parameter::optional(
    "timestamp",
    kind::TIMESTAMP,
    "The timestamp used to generate the UUIDv7.",
)
.default(&DEFAULT_TIMESTAMP)];

#[allow(clippy::cast_sign_loss)] // TODO consider removal options
fn uuid_v7(timestamp: Option<Value>) -> Resolved {
    let utc_timestamp: DateTime<Utc> = if let Some(timestamp) = timestamp {
        timestamp.try_timestamp()?
    } else {
        Utc::now()
    };

    let seconds = utc_timestamp.timestamp() as u64;
    let nanoseconds = match utc_timestamp.timestamp_nanos_opt() {
        Some(_) => utc_timestamp.timestamp_subsec_nanos(),
        None => return Err(ValueError::OutOfRange(Kind::timestamp()).into()),
    };
    let timestamp = Timestamp::from_unix(NoContext, seconds, nanoseconds);

    let mut buffer = [0; 36];
    let uuid = uuid::Uuid::new_v7(timestamp)
        .hyphenated()
        .encode_lower(&mut buffer);
    Ok(Value::from(&*uuid))
}

#[derive(Clone, Copy, Debug)]
pub struct UuidV7;

impl Function for UuidV7 {
    fn identifier(&self) -> &'static str {
        "uuid_v7"
    }

    fn usage(&self) -> &'static str {
        "Generates a random [UUIDv7](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-04#name-uuid-version-7) string."
    }

    fn category(&self) -> &'static str {
        Category::Random.as_ref()
    }

    fn return_kind(&self) -> u16 {
        kind::BYTES
    }

    fn parameters(&self) -> &'static [Parameter] {
        PARAMETERS
    }

    fn examples(&self) -> &'static [Example] {
        &[
            example! {
                title: "Create a UUIDv7 with implicit `now()`",
                source: "uuid_v7()",
                result: Ok("0135ddb4-a444-794c-a7a2-088f260104c0"),
                deterministic: false,
            },
            example! {
                title: "Create a UUIDv7 with explicit `now()`",
                source: "uuid_v7(now())",
                result: Ok("0135ddb4-a444-794c-a7a2-088f260104c0"),
                deterministic: false,
            },
            example! {
                title: "Create a UUIDv7 with custom timestamp",
                source: "uuid_v7(t'2020-12-30T22:20:53.824727Z')",
                result: Ok("0176b5bd-58c0-794c-a7a2-088f260104c0"),
                deterministic: false,
            },
        ]
    }

    fn compile(
        &self,
        _state: &state::TypeState,
        _ctx: &mut FunctionCompileContext,
        arguments: ArgumentList,
    ) -> Compiled {
        let timestamp = arguments.optional("timestamp");

        Ok(UuidV7Fn { timestamp }.as_expr())
    }
}

#[derive(Debug, Clone)]
struct UuidV7Fn {
    timestamp: Option<Box<dyn Expression>>,
}

impl FunctionExpression for UuidV7Fn {
    fn resolve(&self, ctx: &mut Context) -> Resolved {
        let timestamp = self
            .timestamp
            .as_ref()
            .map(|m| m.resolve(ctx))
            .transpose()?;

        uuid_v7(timestamp)
    }

    fn type_def(&self, _: &TypeState) -> TypeDef {
        TypeDef::bytes().infallible()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::value::Value;
    use std::collections::BTreeMap;

    test_type_def![default {
        expr: |_| { UuidV7Fn { timestamp: None } },
        want: TypeDef::bytes().infallible(),
    }];

    #[test]
    fn uuid_v7() {
        let mut state = state::RuntimeState::default();
        let mut object: Value = Value::Object(BTreeMap::new());
        let tz = TimeZone::default();
        let mut ctx = Context::new(&mut object, &mut state, &tz);
        let value = UuidV7Fn { timestamp: None }.resolve(&mut ctx).unwrap();

        uuid::Uuid::parse_str(value.as_string().expect("UUIDv7 must be a string"))
            .expect("valid UUID V7");
    }

    #[test]
    fn uuid_v7_preserves_millisecond_timestamp() {
        for input in [
            "1970-01-01T00:00:00Z",
            "2020-12-30T22:20:53.824727Z",
            "2026-01-01T00:00:00.123456789Z",
            "2026-01-01T00:00:00.999999999Z",
        ] {
            let timestamp: DateTime<Utc> = input.parse().unwrap();
            let expected = (
                u64::try_from(timestamp.timestamp()).unwrap(),
                timestamp.timestamp_subsec_millis() * 1_000_000,
            );

            let encoded = super::uuid_v7(Some(timestamp.into())).unwrap();
            let encoded = encoded.as_string().expect("UUIDv7 must be a string");
            let id = uuid::Uuid::parse_str(encoded).unwrap();
            assert_eq!(id.get_timestamp().unwrap().to_unix(), expected, "{input}");
        }
    }
}