use nautilus_schema::{validate_schema, Lexer, Parser, SchemaError};
fn parse(source: &str) -> Result<nautilus_schema::ast::Schema, SchemaError> {
let mut lexer = Lexer::new(source);
let mut tokens = Vec::new();
loop {
let token = lexer.next_token()?;
let is_eof = matches!(token.kind, nautilus_schema::TokenKind::Eof);
tokens.push(token);
if is_eof {
break;
}
}
Parser::new(&tokens, source).parse_schema()
}
#[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
}
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_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
}
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_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_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_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);
}