use audb::schema::{Field, Schema, Type};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ForeignKeyInfo {
pub field_name: String,
pub target_schema: String,
pub nullable: bool,
}
impl ForeignKeyInfo {
pub fn new(field_name: String, target_schema: String, nullable: bool) -> Self {
Self {
field_name,
target_schema,
nullable,
}
}
pub fn method_name(&self) -> String {
if self.field_name.ends_with("_id") {
self.field_name[..self.field_name.len() - 3].to_string()
} else {
self.field_name.clone()
}
}
}
pub fn detect_foreign_keys(schema: &Schema) -> Vec<ForeignKeyInfo> {
let mut foreign_keys = Vec::new();
for field in &schema.fields {
if !matches!(field.field_type, Type::EntityId) {
continue;
}
if !field.name.ends_with("_id") {
continue;
}
if field.name == "id" {
continue;
}
let entity_name = &field.name[..field.name.len() - 3];
let target_schema = to_pascal_case(entity_name);
foreign_keys.push(ForeignKeyInfo::new(
field.name.clone(),
target_schema,
field.nullable,
));
}
foreign_keys
}
pub fn find_primary_key(schema: &Schema) -> Option<&Field> {
if let Some(field) = schema
.fields
.iter()
.find(|f| f.name == "id" && matches!(f.field_type, Type::EntityId))
{
return Some(field);
}
schema
.fields
.iter()
.find(|f| matches!(f.field_type, Type::EntityId))
}
pub fn to_pascal_case(s: &str) -> String {
if s.is_empty() {
return String::new();
}
s.split('_')
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
None => String::new(),
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use audb::schema::SchemaFormat;
#[test]
fn test_to_pascal_case_simple() {
assert_eq!(to_pascal_case("author"), "Author");
assert_eq!(to_pascal_case("user"), "User");
assert_eq!(to_pascal_case("post"), "Post");
}
#[test]
fn test_to_pascal_case_multiple_words() {
assert_eq!(to_pascal_case("blog_post"), "BlogPost");
assert_eq!(to_pascal_case("parent_category"), "ParentCategory");
assert_eq!(to_pascal_case("user_profile"), "UserProfile");
}
#[test]
fn test_to_pascal_case_edge_cases() {
assert_eq!(to_pascal_case(""), "");
assert_eq!(to_pascal_case("a"), "A");
assert_eq!(to_pascal_case("a_b_c"), "ABC");
}
#[test]
fn test_detect_foreign_keys_none() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("name".to_string(), Type::String));
let fks = detect_foreign_keys(&schema);
assert_eq!(fks.len(), 0);
}
#[test]
fn test_detect_foreign_keys_single() {
let mut schema = Schema::new("Post".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("author_id".to_string(), Type::EntityId));
schema.add_field(Field::new("title".to_string(), Type::String));
let fks = detect_foreign_keys(&schema);
assert_eq!(fks.len(), 1);
assert_eq!(fks[0].field_name, "author_id");
assert_eq!(fks[0].target_schema, "Author");
assert!(!fks[0].nullable);
}
#[test]
fn test_detect_foreign_keys_multiple() {
let mut schema = Schema::new("Comment".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("post_id".to_string(), Type::EntityId));
schema.add_field(Field::new("author_id".to_string(), Type::EntityId));
let fks = detect_foreign_keys(&schema);
assert_eq!(fks.len(), 2);
assert_eq!(fks[0].field_name, "post_id");
assert_eq!(fks[0].target_schema, "Post");
assert_eq!(fks[1].field_name, "author_id");
assert_eq!(fks[1].target_schema, "Author");
}
#[test]
fn test_detect_foreign_keys_nullable() {
let mut schema = Schema::new("Post".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
let mut editor_field = Field::new("editor_id".to_string(), Type::EntityId);
editor_field.nullable = true;
schema.add_field(editor_field);
let fks = detect_foreign_keys(&schema);
assert_eq!(fks.len(), 1);
assert_eq!(fks[0].field_name, "editor_id");
assert_eq!(fks[0].target_schema, "Editor");
assert!(fks[0].nullable);
}
#[test]
fn test_detect_foreign_keys_composite_names() {
let mut schema = Schema::new("Item".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("parent_category_id".to_string(), Type::EntityId));
let fks = detect_foreign_keys(&schema);
assert_eq!(fks.len(), 1);
assert_eq!(fks[0].field_name, "parent_category_id");
assert_eq!(fks[0].target_schema, "ParentCategory");
}
#[test]
fn test_detect_foreign_keys_ignores_non_entity_id() {
let mut schema = Schema::new("Test".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("user_id".to_string(), Type::String)); schema.add_field(Field::new("count_id".to_string(), Type::Integer));
let fks = detect_foreign_keys(&schema);
assert_eq!(fks.len(), 0);
}
#[test]
fn test_detect_foreign_keys_ignores_id() {
let mut schema = Schema::new("Test".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
let fks = detect_foreign_keys(&schema);
assert_eq!(fks.len(), 0); }
#[test]
fn test_find_primary_key_named_id() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("name".to_string(), Type::String));
let pk = find_primary_key(&schema).unwrap();
assert_eq!(pk.name, "id");
}
#[test]
fn test_find_primary_key_alternate_name() {
let mut schema = Schema::new("Entity".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("name".to_string(), Type::String));
schema.add_field(Field::new("entity_id".to_string(), Type::EntityId));
let pk = find_primary_key(&schema).unwrap();
assert_eq!(pk.name, "entity_id");
}
#[test]
fn test_find_primary_key_prefers_id() {
let mut schema = Schema::new("Test".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("user_id".to_string(), Type::EntityId));
schema.add_field(Field::new("id".to_string(), Type::EntityId));
let pk = find_primary_key(&schema).unwrap();
assert_eq!(pk.name, "id"); }
#[test]
fn test_find_primary_key_none() {
let mut schema = Schema::new("Simple".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("name".to_string(), Type::String));
schema.add_field(Field::new("value".to_string(), Type::Integer));
assert!(find_primary_key(&schema).is_none());
}
#[test]
fn test_foreign_key_info_method_name() {
let fk1 = ForeignKeyInfo::new("author_id".to_string(), "Author".to_string(), false);
assert_eq!(fk1.method_name(), "author");
let fk2 = ForeignKeyInfo::new(
"parent_category_id".to_string(),
"ParentCategory".to_string(),
true,
);
assert_eq!(fk2.method_name(), "parent_category");
let fk3 = ForeignKeyInfo::new("user_id".to_string(), "User".to_string(), false);
assert_eq!(fk3.method_name(), "user");
}
#[test]
fn test_foreign_key_info_creation() {
let fk = ForeignKeyInfo::new("author_id".to_string(), "Author".to_string(), true);
assert_eq!(fk.field_name, "author_id");
assert_eq!(fk.target_schema, "Author");
assert!(fk.nullable);
}
}