mod common;
use common::parse_schema as parse;
use nautilus_schema::{validate_schema, SchemaError};
#[test]
fn test_duplicate_model_names() {
let source = r#"
model User {
id Int @id
}
model User {
id Int @id
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("Duplicate model name 'User'"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_duplicate_enum_names() {
let source = r#"
enum Role {
USER
ADMIN
}
enum Role {
STAFF
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("Duplicate enum name 'Role'"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_duplicate_field_names() {
let source = r#"
model User {
id Int @id
email String
email String
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("Duplicate field name 'email'"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_unknown_type() {
let source = r#"
model Post {
id Int @id
author UnknownType
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("Unknown type 'UnknownType'"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_invalid_decimal_precision_zero() {
let source = r#"
model Product {
id Int @id
price Decimal(0, 2)
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("precision must be greater than 0"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_invalid_decimal_scale_exceeds_precision() {
let source = r#"
model Product {
id Int @id
price Decimal(5, 10)
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("scale") && msg.contains("precision"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_composite_pk_nonexistent_field() {
let source = r#"
model User {
id Int @id
@@id([id, nonexistent])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("@@id references non-existent field 'nonexistent'"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_composite_pk_array_field() {
let source = r#"
model User {
id Int @id
tags String[]
@@id([id, tags])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("cannot be an array"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_relation_field_count_mismatch() {
let source = r#"
model User {
id Int @id
}
model Post {
id Int @id
userId Int
extraId Int
user User @relation(fields: [userId, extraId], references: [id])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("has 2 fields but 1 references"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_relation_unknown_target_model() {
let source = r#"
model Post {
id Int @id
author UnknownModel @relation(fields: [authorId], references: [id])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("Unknown type 'UnknownModel'") || msg.contains("unknown model"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_relation_nonexistent_fk_field() {
let source = r#"
model User {
id Int @id
}
model Post {
id Int @id
user User @relation(fields: [nonexistent], references: [id])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("references non-existent field 'nonexistent'"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_relation_nonexistent_reference_field() {
let source = r#"
model User {
id Int @id
}
model Post {
id Int @id
userId Int
user User @relation(fields: [userId], references: [nonexistent])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("references non-existent field 'nonexistent'"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_relation_references_non_unique_field() {
let source = r#"
model User {
id Int @id
email String
}
model Post {
id Int @id
userEmail String
user User @relation(fields: [userEmail], references: [email])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("not a primary key or unique field"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_relation_references_unique_field_ok() {
let source = r#"
model User {
id Int @id
email String @unique
posts Post[]
}
model Post {
id Int @id
userEmail String
user User @relation(fields: [userEmail], references: [email])
}
"#;
let ast = parse(source).unwrap();
let ir = validate_schema(ast).unwrap();
assert_eq!(ir.models.len(), 2);
}
#[test]
fn test_relation_references_composite_primary_key_ok() {
let source = r#"
model User {
firstName String
lastName String
posts Post[]
@@id([firstName, lastName])
}
model Post {
id Int @id
userFirstName String
userLastName String
user User @relation(fields: [userFirstName, userLastName], references: [firstName, lastName])
}
"#;
let ast = parse(source).unwrap();
let ir = validate_schema(ast).unwrap();
assert_eq!(ir.models.len(), 2);
}
#[test]
fn test_relation_references_composite_unique_ok() {
let source = r#"
model User {
id Int @id
email String
username String
posts Post[]
@@unique([email, username])
}
model Post {
id Int @id
userEmail String
userUsername String
user User @relation(fields: [userEmail, userUsername], references: [email, username])
}
"#;
let ast = parse(source).unwrap();
let ir = validate_schema(ast).unwrap();
assert_eq!(ir.models.len(), 2);
}
#[test]
fn test_relation_references_non_unique_composite_fields() {
let source = r#"
model User {
id Int @id
email String
username String
posts Post[]
}
model Post {
id Int @id
userEmail String
userUsername String
user User @relation(fields: [userEmail, userUsername], references: [email, username])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("composite primary key or unique constraint"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_multiple_relations_without_name() {
let source = r#"
model User {
id Int @id
}
model Post {
id Int @id
authorId Int
reviewerId Int
author User @relation(fields: [authorId], references: [id])
reviewer User @relation(fields: [reviewerId], references: [id])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("multiple relations") && msg.contains("unique 'name' parameters"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_multiple_relations_with_name_ok() {
let source = r#"
model User {
id Int @id
authoredPosts Post[] @relation(name: "AuthoredPosts")
reviewedPosts Post[] @relation(name: "ReviewedPosts")
}
model Post {
id Int @id
authorId Int
reviewerId Int
author User @relation(name: "AuthoredPosts", fields: [authorId], references: [id])
reviewer User @relation(name: "ReviewedPosts", fields: [reviewerId], references: [id])
}
"#;
let ast = parse(source).unwrap();
let ir = validate_schema(ast).unwrap();
assert_eq!(ir.models.len(), 2);
}
#[test]
fn test_named_relations_cannot_share_one_opposite_field() {
let source = r#"
model User {
id Int @id
authoredPosts Post[] @relation(name: "AuthoredPosts")
}
model Post {
id Int @id
authorId Int
reviewerId Int
author User @relation(name: "AuthoredPosts", fields: [authorId], references: [id])
reviewer User @relation(name: "ReviewedPosts", fields: [reviewerId], references: [id])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(
msg.contains("expects an opposite relation field")
|| msg.contains("missing an opposite relation field"),
"unexpected message: {}",
msg
);
assert!(msg.contains("ReviewedPosts"), "unexpected message: {}", msg);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_default_enum_variant_not_found() {
let source = r#"
enum Role {
USER
ADMIN
}
model User {
id Int @id
role Role @default(INVALID)
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("variant 'INVALID'"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_default_enum_variant_ok() {
let source = r#"
enum Role {
USER
ADMIN
}
model User {
id Int @id
role Role @default(USER)
}
"#;
let ast = parse(source).unwrap();
let ir = validate_schema(ast).unwrap();
assert_eq!(ir.enums.len(), 1);
}
#[test]
fn test_default_type_mismatch_string() {
let source = r#"
model User {
id Int @id
name String @default(123)
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("Type mismatch"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_default_autoincrement_on_string() {
let source = r#"
model User {
id String @id @default(autoincrement())
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("autoincrement()") && msg.contains("Int or BigInt"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_default_uuid_on_int() {
let source = r#"
model User {
id Int @id @default(uuid())
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("uuid()") && msg.contains("Uuid"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_default_now_on_int() {
let source = r#"
model User {
id Int @id @default(now())
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("now()") && msg.contains("DateTime"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_physical_table_name_collision() {
let source = r#"
model User {
id Int @id
@@map("people")
}
model Person {
id Int @id
@@map("people")
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("Physical table name 'people'"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_physical_column_name_collision() {
let source = r#"
model User {
id Int @id
userName String @map("name")
fullName String @map("name")
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("Physical column name 'name'"));
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_datasource_missing_provider() {
let source = r#"
datasource db {
url = "postgres://localhost/test"
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("missing required 'provider'"), "got: {}", msg);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_datasource_missing_url() {
let source = r#"
datasource db {
provider = "postgresql"
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("missing required 'url'"), "got: {}", msg);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_datasource_unknown_field() {
let source = r#"
datasource db {
provider = "postgresql"
url = "postgres://localhost/test"
foo = "bar"
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("Unknown field 'foo'"), "got: {}", msg);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_datasource_accepts_direct_url() {
let source = r#"
datasource db {
provider = "postgresql"
url = "postgres://localhost/test"
direct_url = env("DIRECT_DATABASE_URL")
}
model User {
id Int @id
}
"#;
let ast = parse(source).unwrap();
let ir = validate_schema(ast).expect("direct_url should validate");
assert_eq!(
ir.datasource
.as_ref()
.and_then(|ds| ds.direct_url.as_deref()),
Some("env(DIRECT_DATABASE_URL)")
);
}
#[test]
fn test_generator_missing_provider() {
let source = r#"
generator client {
output = "../generated"
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("missing required 'provider'"), "got: {}", msg);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_generator_unknown_field() {
let source = r#"
generator client {
provider = "nautilus-client-rs"
output = "../generated"
foo = "bar"
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(msg.contains("Unknown field 'foo'"), "got: {}", msg);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_generator_output_must_be_string_literal() {
let source = r#"
generator client {
provider = "nautilus-client-rs"
output = 123
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(
msg.contains("Generator 'output' must be a string literal"),
"got: {}",
msg
);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_generator_interface_must_be_string_literal() {
let source = r#"
generator client {
provider = "nautilus-client-rs"
interface = 123
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(
msg.contains("Generator 'interface' must be a string literal"),
"got: {}",
msg
);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_generator_recursive_type_depth_python_only_error_on_rust() {
let source = r#"
generator client {
provider = "nautilus-client-rs"
recursive_type_depth = 3
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(
msg.contains("recursive_type_depth") && msg.contains("nautilus-client-py"),
"got: {}",
msg
);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_generator_recursive_type_depth_python_only_error_on_js() {
let source = r#"
generator client {
provider = "nautilus-client-js"
recursive_type_depth = 3
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(
msg.contains("recursive_type_depth") && msg.contains("nautilus-client-py"),
"got: {}",
msg
);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_generator_recursive_type_depth_valid_for_python() {
let source = r#"
generator client {
provider = "nautilus-client-py"
output = "../generated"
recursive_type_depth = 3
}
"#;
let ast = parse(source).unwrap();
let ir = validate_schema(ast).unwrap();
let gen = ir.generator.as_ref().unwrap();
assert_eq!(gen.recursive_type_depth, 3);
}
#[test]
fn test_missing_back_relation_error() {
let source = r#"
model User {
id Int @id
}
model Post {
id Int @id
authorId Int
author User @relation(fields: [authorId], references: [id])
}
"#;
let ast = parse(source).unwrap();
let err = validate_schema(ast).unwrap_err();
match err {
SchemaError::Validation(msg, _) => {
assert!(
msg.contains("missing an opposite relation field") && msg.contains("Post[]"),
"unexpected message: {}",
msg
);
}
_ => panic!("Expected validation error"),
}
}
#[test]
fn test_back_relation_present_ok() {
let source = r#"
model User {
id Int @id
posts Post[]
}
model Post {
id Int @id
authorId Int
author User @relation(fields: [authorId], references: [id])
}
"#;
let ast = parse(source).unwrap();
let ir = validate_schema(ast).unwrap();
assert_eq!(ir.models.len(), 2);
}