Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ULID support #5092

Merged
merged 9 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ derive_more = "0.99.17"
user-facing-errors = { path = "./libs/user-facing-errors" }
uuid = { version = "1", features = ["serde", "v4", "v7", "js"] }
cuid = { git = "https://github.com/prisma/cuid-rust", branch = "v1.3.3-wasm32-unknown-unknown" }
ulid = "1"
getrandom = { version = "0.2" }

indoc = "2.0.1"
Expand All @@ -78,7 +79,7 @@ tsify = { version = "0.4.5" }

# version for `[email protected]`, see:
# https://github.com/rustwasm/wasm-bindgen/pull/4072/
js-sys = { version = "0.3.70" }
js-sys = { version = "0.3.70" }

wasm-bindgen = { version = "0.2.93" }
wasm-bindgen-futures = { version = "0.4" }
Expand Down
8 changes: 8 additions & 0 deletions psl/parser-database/src/attributes/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ 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_ULID => {
validate_empty_function_args(funcname, &funcargs.arguments, accept, ctx)
}
(ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) if funcname == FN_CUID => {
validate_uid_int_args(funcname, &funcargs.arguments, &CUID_SUPPORTED_VERSIONS, accept, ctx)
}
Expand Down Expand Up @@ -244,6 +247,9 @@ fn validate_composite_builtin_scalar_type_default(
) {
match (scalar_type, value) {
// Functions
(ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) if funcname == FN_ULID => {
validate_empty_function_args(funcname, &funcargs.arguments, accept, ctx)
}
(ScalarType::String, ast::Expression::Function(funcname, funcargs, _)) if funcname == FN_CUID => {
validate_uid_int_args(funcname, &funcargs.arguments, &CUID_SUPPORTED_VERSIONS, accept, ctx)
}
Expand Down Expand Up @@ -526,6 +532,7 @@ fn validate_builtin_scalar_list_default(

const FN_AUTOINCREMENT: &str = "autoincrement";
const FN_CUID: &str = "cuid";
const FN_ULID: &str = "ulid";
const FN_DBGENERATED: &str = "dbgenerated";
const FN_NANOID: &str = "nanoid";
const FN_NOW: &str = "now";
Expand All @@ -534,6 +541,7 @@ const FN_AUTO: &str = "auto";

const KNOWN_FUNCTIONS: &[&str] = &[
FN_AUTOINCREMENT,
FN_ULID,
FN_CUID,
FN_DBGENERATED,
FN_NANOID,
Expand Down
5 changes: 5 additions & 0 deletions psl/parser-database/src/walkers/scalar_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ impl<'db> DefaultValueWalker<'db> {
matches!(self.value(), ast::Expression::Function(name, _, _) if name == "autoincrement")
}

/// Is this an `@default(cuid())`?
aqrln marked this conversation as resolved.
Show resolved Hide resolved
pub fn is_ulid(self) -> bool {
matches!(self.value(), ast::Expression::Function(name, _, _) if name == "ulid")
}

/// Is this an `@default(cuid())`?
pub fn is_cuid(self) -> bool {
matches!(self.value(), ast::Expression::Function(name, _, _) if name == "cuid")
Expand Down
28 changes: 24 additions & 4 deletions psl/psl/tests/attributes/id_positive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,26 @@ fn should_allow_string_ids_with_uuid_version_specified() {
}
}

#[test]
fn should_allow_string_ids_with_ulid() {
let dml = indoc! {r#"
model Model {
id String @id @default(ulid())
}
"#};

let schema = psl::parse_schema(dml).unwrap();
let model = schema.assert_has_model("Model");

model
.assert_has_scalar_field("id")
.assert_scalar_type(ScalarType::String)
.assert_default_value()
.assert_ulid();

model.assert_id_on_fields(&["id"]);
}

#[test]
fn should_allow_string_ids_without_default() {
let dml = indoc! {r#"
Expand Down Expand Up @@ -317,10 +337,10 @@ fn id_accepts_length_arg_on_mysql() {
firstName String
middleName String
lastName String

@@id([firstName, middleName(length: 1), lastName])
}

model Blog {
title String @id(length:5)
}
Expand Down Expand Up @@ -348,10 +368,10 @@ fn id_accepts_sort_arg_on_sqlserver() {
firstName String
middleName String
lastName String

@@id([firstName, middleName(sort: Desc), lastName])
}

model Blog {
title String @id(sort: Desc)
}
Expand Down
14 changes: 14 additions & 0 deletions psl/psl/tests/common/asserts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub(crate) trait DefaultValueAssert {
fn assert_constant(&self, val: &str) -> &Self;
fn assert_bytes(&self, val: &[u8]) -> &Self;
fn assert_now(&self) -> &Self;
fn assert_ulid(&self) -> &Self;
fn assert_cuid(&self) -> &Self;
fn assert_cuid_version(&self, version: u8) -> &Self;
fn assert_uuid(&self) -> &Self;
Expand Down Expand Up @@ -429,6 +430,12 @@ impl DefaultValueAssert for walkers::DefaultValueWalker<'_> {
self
}

#[track_caller]
fn assert_ulid(&self) -> &Self {
self.value().assert_ulid();
self
}

#[track_caller]
fn assert_cuid(&self) -> &Self {
self.value().assert_cuid();
Expand Down Expand Up @@ -634,6 +641,13 @@ impl DefaultValueAssert for ast::Expression {
self
}

#[track_caller]
fn assert_ulid(&self) -> &Self {
assert!(matches!(self, ast::Expression::Function(name, _, _) if name == "ulid"));

self
}

#[track_caller]
fn assert_cuid(&self) -> &Self {
assert!(matches!(self, ast::Expression::Function(name, _, _) if name == "cuid"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
datasource db {
provider = "mysql"
url = "does_not_matter"
}

model Category {
id Int @id @default(ulid())
}

// error: Error parsing attribute "@default": The function `ulid()` cannot be used on fields of type `Int`.
// --> schema.prisma:7
//  | 
//  6 | model Category {
//  7 |  id Int @id @default(ulid())
//  | 
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
datasource db {
provider = "mysql"
url = "does_not_matter"
}

model Category {
id String @id @default(ulid())
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ psl.workspace = true
base64 = "0.13"
uuid.workspace = true
cuid.workspace = true
ulid.workspace = true
tokio.workspace = true
user-facing-errors.workspace = true
prisma-value = { path = "../../../libs/prisma-value" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,57 @@ mod uuid_create_graphql {

Ok(())
}

fn schema_ulid() -> String {
let schema = indoc! {
r#"model Todo {
#id(id, String, @id, @default(ulid()))
title String
}"#
};

schema.to_owned()
}

// "Creating an item with an id field of model ULID and retrieving it" should "work"
#[connector_test(schema(schema_ulid))]
async fn create_ulid_and_retrieve_it_should_work(runner: Runner) -> TestResult<()> {
let res = run_query_json!(
&runner,
r#"mutation {
createOneTodo(data: { title: "the title" }){
id
}
}"#
);

let ulid = res["data"]["createOneTodo"]["id"]
.as_str()
.expect("Expected string ID but got something else.");

// Validate that this is a valid ULID value
assert!(ulid::Ulid::from_string(ulid).is_ok());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, add an explicit test to ensure we emit 26-char-long ULIDs, thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


// Test findMany
let res = run_query_json!(
&runner,
r#"query { findManyTodo(where: { title: "the title" }) { id }}"#
);
let ulid_find_many = res["data"]["findManyTodo"][0]["id"]
.as_str()
.expect("Expected string ID but got something else.");
assert_eq!(ulid_find_many, ulid);

// Test findUnique
let res = run_query_json!(
&runner,
format!(r#"query {{ findUniqueTodo(where: {{ id: "{}" }}) {{ id }} }}"#, ulid)
);
let ulid_find_unique = res["data"]["findUniqueTodo"]["id"]
.as_str()
.expect("Expected string ID but got something else.");
assert_eq!(ulid_find_unique, ulid);

Ok(())
}
}
1 change: 1 addition & 0 deletions query-engine/connectors/sql-query-connector/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ tracing = { workspace = true, features = ["log"] }
tracing-futures.workspace = true
uuid.workspace = true
cuid.workspace = true
ulid.workspace = true
quaint.workspace = true

[dev-dependencies]
Expand Down
1 change: 1 addition & 0 deletions query-engine/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
user-facing-errors = { path = "../../libs/user-facing-errors" }
uuid.workspace = true
cuid.workspace = true
ulid.workspace = true
schema = { path = "../schema" }
crosstarget-utils = { path = "../../libs/crosstarget-utils" }
telemetry = { path = "../../libs/telemetry" }
Expand Down
Loading
Loading