diff --git a/Cargo.lock b/Cargo.lock index fbe4a8ffb542..69d2a235209c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6074,12 +6074,13 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.11", "serde", + "wasm-bindgen", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fbba97fcc427..34804d5daaa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ tokio = { version = "1.25", features = [ ] } chrono = { version = "0.4.38", features = ["serde"] } user-facing-errors = { path = "./libs/user-facing-errors" } -uuid = { version = "1", features = ["serde", "v4"] } +uuid = { version = "1", features = ["serde", "v4", "v7", "js"] } indoc = "2.0.1" indexmap = { version = "2.2.2", features = ["serde"] } itertools = "0.12" diff --git a/psl/parser-database/src/attributes/default.rs b/psl/parser-database/src/attributes/default.rs index e2be240f152c..d1f6b887fa22 100644 --- a/psl/parser-database/src/attributes/default.rs +++ b/psl/parser-database/src/attributes/default.rs @@ -196,11 +196,12 @@ fn validate_model_builtin_scalar_type_default( { validate_empty_function_args(funcname, &funcargs.arguments, accept, ctx) } - (ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) - if funcname == FN_UUID || funcname == FN_CUID => - { + (ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) if funcname == FN_CUID => { validate_empty_function_args(funcname, &funcargs.arguments, accept, ctx) } + (ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) if funcname == FN_UUID => { + validate_uuid_args(&funcargs.arguments, accept, ctx) + } (ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) if funcname == FN_NANOID => { validate_nanoid_args(&funcargs.arguments, accept, ctx) } @@ -242,11 +243,12 @@ fn validate_composite_builtin_scalar_type_default( ) { match (scalar_type, value) { // Functions - (ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) - if funcname == FN_UUID || funcname == FN_CUID => - { + (ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) if funcname == FN_CUID => { validate_empty_function_args(funcname, &funcargs.arguments, accept, ctx) } + (ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) if funcname == FN_UUID => { + validate_uuid_args(&funcargs.arguments, accept, ctx) + } (ScalarType::DateTime, ast::Expression::Function(funcname, funcargs, _)) if funcname == FN_NOW => { validate_empty_function_args(FN_NOW, &funcargs.arguments, accept, ctx) } @@ -379,6 +381,24 @@ fn validate_dbgenerated_args(args: &[ast::Argument], accept: AcceptFn<'_>, ctx: } } +fn validate_uuid_args(args: &[ast::Argument], accept: AcceptFn<'_>, ctx: &mut Context<'_>) { + let mut bail = || ctx.push_attribute_validation_error("`uuid()` takes a single Int argument."); + + if args.len() > 1 { + bail() + } + + match args.first().map(|arg| &arg.value) { + Some(ast::Expression::NumericValue(val, _)) if ![4u8, 7u8].contains(&val.parse::().unwrap()) => { + ctx.push_attribute_validation_error( + "`uuid()` takes either no argument, or a single integer argument which is either 4 or 7.", + ); + } + None | Some(ast::Expression::NumericValue(_, _)) => accept(ctx), + _ => bail(), + } +} + fn validate_nanoid_args(args: &[ast::Argument], accept: AcceptFn<'_>, ctx: &mut Context<'_>) { let mut bail = || ctx.push_attribute_validation_error("`nanoid()` takes a single Int argument."); diff --git a/psl/psl/tests/attributes/id_negative.rs b/psl/psl/tests/attributes/id_negative.rs index ce4a1ccbaa4d..bc3c9785118c 100644 --- a/psl/psl/tests/attributes/id_negative.rs +++ b/psl/psl/tests/attributes/id_negative.rs @@ -44,6 +44,26 @@ fn id_should_error_multiple_ids_are_provided() { expect_error(dml, &expectation) } +#[test] +fn id_should_error_on_invalid_uuid_version() { + let dml = indoc! {r#" + model Model { + id String @id @default(uuid(1)) + } + "#}; + + let expectation = expect![[r#" + error: Error parsing attribute "@default": `uuid()` takes either no argument, or a single integer argument which is either 4 or 7. + --> schema.prisma:2 +  |  +  1 | model Model { +  2 |  id String @id @default(uuid(1)) +  |  + "#]]; + + expect_error(dml, &expectation) +} + #[test] fn id_must_error_when_single_and_multi_field_id_is_used() { let dml = indoc! {r#" diff --git a/psl/psl/tests/attributes/id_positive.rs b/psl/psl/tests/attributes/id_positive.rs index 396c334f94fd..e81c1305c274 100644 --- a/psl/psl/tests/attributes/id_positive.rs +++ b/psl/psl/tests/attributes/id_positive.rs @@ -70,6 +70,45 @@ fn should_allow_string_ids_with_uuid() { model.assert_id_on_fields(&["id"]); } +#[test] +fn should_allow_string_ids_with_uuid_version_specified() { + let dml = indoc! {r#" + model ModelA { + id String @id @default(uuid(4)) + } + + model ModelB { + id String @id @default(uuid(7)) + } + "#}; + + let schema = psl::parse_schema(dml).unwrap(); + + { + let model = schema.assert_has_model("ModelA"); + + model + .assert_has_scalar_field("id") + .assert_scalar_type(ScalarType::String) + .assert_default_value() + .assert_uuid(); + + model.assert_id_on_fields(&["id"]); + } + + { + let model = schema.assert_has_model("ModelB"); + + model + .assert_has_scalar_field("id") + .assert_scalar_type(ScalarType::String) + .assert_default_value() + .assert_uuid(); + + model.assert_id_on_fields(&["id"]); + } +} + #[test] fn should_allow_string_ids_without_default() { let dml = indoc! {r#" diff --git a/psl/psl/tests/common/asserts.rs b/psl/psl/tests/common/asserts.rs index 4278f5cb77e5..e75df7c2cd7b 100644 --- a/psl/psl/tests/common/asserts.rs +++ b/psl/psl/tests/common/asserts.rs @@ -631,9 +631,7 @@ impl DefaultValueAssert for ast::Expression { #[track_caller] fn assert_uuid(&self) -> &Self { - assert!( - matches!(self, ast::Expression::Function(name, args, _) if name == "uuid" && args.arguments.is_empty()) - ); + assert!(matches!(self, ast::Expression::Function(name, _, _) if name == "uuid")); self } diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/ids/uuid_create_graphql.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/ids/uuid_create_graphql.rs index 5efd17555673..afacaa5292b5 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/ids/uuid_create_graphql.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/ids/uuid_create_graphql.rs @@ -68,4 +68,66 @@ mod uuid_create_graphql { Ok(()) } + + fn schema_3() -> String { + let schema = indoc! { + r#"model Todo { + #id(id, String, @id, @default(uuid(7))) + title String + }"# + }; + + schema.to_owned() + } + + // "Creating an item with an id field of model UUIDv7 and retrieving it" should "work" + #[connector_test(schema(schema_3))] + async fn create_uuid_v7_and_retrieve_it_should_work(runner: Runner) -> TestResult<()> { + let res = run_query_json!( + &runner, + r#"mutation { + createOneTodo(data: { title: "the title" }){ + id + } + }"# + ); + + let uuid = match &res["data"]["createOneTodo"]["id"] { + serde_json::Value::String(str) => str, + _ => unreachable!(), + }; + + // Validate that this is a valid UUIDv7 value + { + let uuid = uuid::Uuid::parse_str(uuid.as_str()).expect("Expected valid UUID but couldn't parse it."); + assert_eq!( + uuid.get_version().expect("Expected UUIDv7 but got something else."), + uuid::Version::SortRand + ); + } + + // Test findMany + let res = run_query_json!( + &runner, + r#"query { findManyTodo(where: { title: "the title" }) { id }}"# + ); + if let serde_json::Value::String(str) = &res["data"]["findManyTodo"][0]["id"] { + assert_eq!(str, uuid); + } else { + panic!("Expected UUID but got something else."); + } + + // Test findUnique + let res = run_query_json!( + &runner, + format!(r#"query {{ findUniqueTodo(where: {{ id: "{}" }}) {{ id }} }}"#, uuid) + ); + if let serde_json::Value::String(str) = &res["data"]["findUniqueTodo"]["id"] { + assert_eq!(str, uuid); + } else { + panic!("Expected UUID but got something else."); + } + + Ok(()) + } } diff --git a/query-engine/query-structure/Cargo.toml b/query-engine/query-structure/Cargo.toml index f990c48ffd4b..847d97bee2ff 100644 --- a/query-engine/query-structure/Cargo.toml +++ b/query-engine/query-structure/Cargo.toml @@ -22,4 +22,4 @@ features = ["js"] [features] # Support for generating default UUID, CUID, nanoid and datetime values. -default_generators = ["uuid/v4", "cuid", "nanoid"] +default_generators = ["uuid/v4", "uuid/v7", "cuid", "nanoid"] diff --git a/query-engine/query-structure/src/default_value.rs b/query-engine/query-structure/src/default_value.rs index 9eaf6828d1c8..605224909e31 100644 --- a/query-engine/query-structure/src/default_value.rs +++ b/query-engine/query-structure/src/default_value.rs @@ -45,7 +45,7 @@ impl DefaultKind { /// Does this match @default(uuid(_))? pub fn is_uuid(&self) -> bool { - matches!(self, DefaultKind::Expression(generator) if generator.name == "uuid") + matches!(self, DefaultKind::Expression(generator) if generator.name.starts_with("uuid")) } pub fn unwrap_single(self) -> PrismaValue { @@ -186,8 +186,8 @@ impl ValueGenerator { ValueGenerator::new("cuid".to_owned(), vec![]).unwrap() } - pub fn new_uuid() -> Self { - ValueGenerator::new("uuid".to_owned(), vec![]).unwrap() + pub fn new_uuid(version: u8) -> Self { + ValueGenerator::new(format!("uuid({version})"), vec![]).unwrap() } pub fn new_nanoid(length: Option) -> Self { @@ -238,7 +238,7 @@ impl ValueGenerator { #[derive(Clone, Copy, PartialEq)] pub enum ValueGeneratorFn { - Uuid, + Uuid(u8), Cuid, Nanoid(Option), Now, @@ -251,7 +251,8 @@ impl ValueGeneratorFn { fn new(name: &str) -> std::result::Result { match name { "cuid" => Ok(Self::Cuid), - "uuid" => Ok(Self::Uuid), + "uuid" | "uuid(4)" => Ok(Self::Uuid(4)), + "uuid(7)" => Ok(Self::Uuid(7)), "now" => Ok(Self::Now), "autoincrement" => Ok(Self::Autoincrement), "sequence" => Ok(Self::Autoincrement), @@ -265,7 +266,7 @@ impl ValueGeneratorFn { #[cfg(feature = "default_generators")] fn invoke(&self) -> Option { match self { - Self::Uuid => Some(Self::generate_uuid()), + Self::Uuid(version) => Some(Self::generate_uuid(*version)), Self::Cuid => Some(Self::generate_cuid()), Self::Nanoid(length) => Some(Self::generate_nanoid(length)), Self::Now => Some(Self::generate_now()), @@ -282,8 +283,12 @@ impl ValueGeneratorFn { } #[cfg(feature = "default_generators")] - fn generate_uuid() -> PrismaValue { - PrismaValue::Uuid(uuid::Uuid::new_v4()) + fn generate_uuid(version: u8) -> PrismaValue { + PrismaValue::Uuid(match version { + 4 => uuid::Uuid::new_v4(), + 7 => uuid::Uuid::now_v7(), + _ => panic!("Unknown UUID version: {}", version), + }) } #[cfg(feature = "default_generators")] @@ -337,8 +342,16 @@ mod tests { } #[test] - fn default_value_is_uuid() { - let uuid_default = DefaultValue::new_expression(ValueGenerator::new_uuid()); + fn default_value_is_uuidv4() { + let uuid_default = DefaultValue::new_expression(ValueGenerator::new_uuid(4)); + + assert!(uuid_default.is_uuid()); + assert!(!uuid_default.is_autoincrement()); + } + + #[test] + fn default_value_is_uuidv7() { + let uuid_default = DefaultValue::new_expression(ValueGenerator::new_uuid(7)); assert!(uuid_default.is_uuid()); assert!(!uuid_default.is_autoincrement()); diff --git a/query-engine/query-structure/src/field/scalar.rs b/query-engine/query-structure/src/field/scalar.rs index 8aeb9c7f47aa..5fc10acddd13 100644 --- a/query-engine/query-structure/src/field/scalar.rs +++ b/query-engine/query-structure/src/field/scalar.rs @@ -244,8 +244,15 @@ pub fn dml_default_kind(default_value: &ast::Expression, scalar_type: Option { DefaultKind::Expression(ValueGenerator::new_sequence(Vec::new())) } - ast::Expression::Function(funcname, _args, _) if funcname == "uuid" => { - DefaultKind::Expression(ValueGenerator::new_uuid()) + ast::Expression::Function(funcname, args, _) if funcname == "uuid" => { + let version = args + .arguments + .first() + .and_then(|arg| arg.value.as_numeric_value()) + .map(|(val, _)| val.parse::().unwrap()) + .unwrap_or(4); + + DefaultKind::Expression(ValueGenerator::new_uuid(version)) } ast::Expression::Function(funcname, _args, _) if funcname == "cuid" => { DefaultKind::Expression(ValueGenerator::new_cuid())