Skip to content

Commit

Permalink
feat: add support for uuid v7 (#4877)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcuelenaere authored Aug 1, 2024
1 parent 714bbd4 commit 4c784e3
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 25 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
32 changes: 26 additions & 6 deletions psl/parser-database/src/attributes/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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::<u8>().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.");

Expand Down
20 changes: 20 additions & 0 deletions psl/psl/tests/attributes/id_negative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down
39 changes: 39 additions & 0 deletions psl/psl/tests/attributes/id_positive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down
4 changes: 1 addition & 3 deletions psl/psl/tests/common/asserts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
}
2 changes: 1 addition & 1 deletion query-engine/query-structure/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
33 changes: 23 additions & 10 deletions query-engine/query-structure/src/default_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<u8>) -> Self {
Expand Down Expand Up @@ -238,7 +238,7 @@ impl ValueGenerator {

#[derive(Clone, Copy, PartialEq)]
pub enum ValueGeneratorFn {
Uuid,
Uuid(u8),
Cuid,
Nanoid(Option<u8>),
Now,
Expand All @@ -251,7 +251,8 @@ impl ValueGeneratorFn {
fn new(name: &str) -> std::result::Result<Self, String> {
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),
Expand All @@ -265,7 +266,7 @@ impl ValueGeneratorFn {
#[cfg(feature = "default_generators")]
fn invoke(&self) -> Option<PrismaValue> {
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()),
Expand All @@ -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")]
Expand Down Expand Up @@ -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());
Expand Down
11 changes: 9 additions & 2 deletions query-engine/query-structure/src/field/scalar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,15 @@ pub fn dml_default_kind(default_value: &ast::Expression, scalar_type: Option<Sca
ast::Expression::Function(funcname, _args, _) if funcname == "sequence" => {
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::<u8>().unwrap())
.unwrap_or(4);

DefaultKind::Expression(ValueGenerator::new_uuid(version))
}
ast::Expression::Function(funcname, _args, _) if funcname == "cuid" => {
DefaultKind::Expression(ValueGenerator::new_cuid())
Expand Down

0 comments on commit 4c784e3

Please sign in to comment.