use crate::foreign_keys::ForeignKeyDefinition;
use crate::types::Type;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, thiserror::Error)]
pub enum SchemaError {
#[error("Failed to read schema file: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to parse YAML: {0}")]
YamlError(#[from] serde_yaml::Error),
#[error("Table not found: {0}")]
TableNotFound(String),
#[error("Field '{field}' not found in table '{table}'")]
FieldNotFound { table: String, field: String },
#[error("Column '{column}' not found in table '{table}'")]
ColumnNotFound { table: String, column: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ColumnDefinition {
pub name: String,
#[serde(rename = "type")]
pub column_type: Type,
#[serde(default)]
pub nullable: bool,
}
impl ColumnDefinition {
pub fn new(name: impl Into<String>, column_type: Type) -> Self {
Self {
name: name.into(),
column_type,
nullable: false,
}
}
pub fn nullable(name: impl Into<String>, column_type: Type) -> Self {
Self {
name: name.into(),
column_type,
nullable: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableDefinition {
pub name: String,
pub primary_key: ColumnDefinition,
pub columns: Vec<ColumnDefinition>,
#[serde(default)]
pub foreign_keys: Vec<ForeignKeyDefinition>,
#[serde(default)]
pub composite_primary_key: Option<Vec<String>>,
}
impl TableDefinition {
pub fn new(
name: impl Into<String>,
primary_key: ColumnDefinition,
columns: Vec<ColumnDefinition>,
) -> Self {
Self {
name: name.into(),
primary_key,
columns,
foreign_keys: Vec::new(),
composite_primary_key: None,
}
}
pub fn get_column(&self, name: &str) -> Option<&ColumnDefinition> {
if self.primary_key.name == name {
return Some(&self.primary_key);
}
self.columns.iter().find(|c| c.name == name)
}
pub fn get_column_type(&self, name: &str) -> Option<&Type> {
self.get_column(name).map(|c| &c.column_type)
}
pub fn column_names(&self) -> Vec<&str> {
let mut names = vec![self.primary_key.name.as_str()];
names.extend(self.columns.iter().map(|c| c.name.as_str()));
names
}
pub fn primary_key_column_names(&self) -> Vec<&str> {
if let Some(ref cpk) = self.composite_primary_key {
cpk.iter().map(|s| s.as_str()).collect()
} else {
vec![self.primary_key.name.as_str()]
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DatabaseSchema {
pub tables: Vec<TableDefinition>,
#[serde(skip)]
table_map: HashMap<String, usize>,
}
impl DatabaseSchema {
pub fn new(tables: Vec<TableDefinition>) -> Self {
let mut schema = Self {
tables,
table_map: HashMap::new(),
};
schema.build_table_map();
schema
}
fn build_table_map(&mut self) {
self.table_map = self
.tables
.iter()
.enumerate()
.map(|(idx, table)| (table.name.clone(), idx))
.collect();
}
pub fn get_table(&self, name: &str) -> Option<&TableDefinition> {
self.table_map
.get(name)
.and_then(|&idx| self.tables.get(idx))
}
pub fn get_table_mut(&mut self, name: &str) -> Option<&mut TableDefinition> {
let idx = *self.table_map.get(name)?;
self.tables.get_mut(idx)
}
pub fn get_column_type(&self, table: &str, column: &str) -> Result<&Type, SchemaError> {
let table_schema = self
.get_table(table)
.ok_or_else(|| SchemaError::TableNotFound(table.to_string()))?;
table_schema
.get_column_type(column)
.ok_or_else(|| SchemaError::ColumnNotFound {
table: table.to_string(),
column: column.to_string(),
})
}
pub fn table_names(&self) -> Vec<&str> {
self.tables.iter().map(|t| t.name.as_str()).collect()
}
pub fn add_table(&mut self, table: TableDefinition) {
let idx = self.tables.len();
self.table_map.insert(table.name.clone(), idx);
self.tables.push(table);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum GeneratorConfig {
UuidV4,
Sequential {
#[serde(default)]
start: i64,
},
Pattern {
pattern: String,
},
IntRange {
min: i64,
max: i64,
},
FloatRange {
min: f64,
max: f64,
},
DecimalRange {
min: f64,
max: f64,
},
TimestampRange {
start: String,
end: String,
},
TimestampNow,
WeightedBool {
true_weight: f64,
},
OneOf {
values: Vec<serde_yaml::Value>,
},
SampleArray {
pool: Vec<String>,
#[serde(default)]
min_length: usize,
max_length: usize,
},
Static {
value: serde_yaml::Value,
},
Null,
DurationRange {
min_secs: u64,
max_secs: u64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratorFieldDefinition {
pub name: String,
#[serde(rename = "type")]
pub field_type: Type,
pub generator: GeneratorConfig,
#[serde(default)]
pub nullable: bool,
}
impl GeneratorFieldDefinition {
pub fn to_column_definition(&self) -> ColumnDefinition {
ColumnDefinition {
name: self.name.clone(),
column_type: self.field_type.clone(),
nullable: self.nullable,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratorIDDefinition {
#[serde(rename = "type")]
pub id_type: Type,
pub generator: GeneratorConfig,
}
impl GeneratorIDDefinition {
pub fn to_column_definition(&self) -> ColumnDefinition {
ColumnDefinition {
name: "id".to_string(),
column_type: self.id_type.clone(),
nullable: false, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratorTableDefinition {
pub name: String,
pub id: GeneratorIDDefinition,
pub fields: Vec<GeneratorFieldDefinition>,
}
impl GeneratorTableDefinition {
pub fn get_field(&self, name: &str) -> Option<&GeneratorFieldDefinition> {
self.fields.iter().find(|f| f.name == name)
}
pub fn get_field_type(&self, name: &str) -> Option<&Type> {
self.get_field(name).map(|f| &f.field_type)
}
pub fn field_names(&self) -> Vec<&str> {
self.fields.iter().map(|f| f.name.as_str()).collect()
}
pub fn to_table_definition(&self) -> TableDefinition {
TableDefinition {
name: self.name.clone(),
primary_key: self.id.to_column_definition(),
columns: self
.fields
.iter()
.map(|f| f.to_column_definition())
.collect(),
foreign_keys: Vec::new(),
composite_primary_key: None,
}
}
}
fn default_version() -> u32 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratorSchema {
#[serde(default = "default_version")]
pub version: u32,
pub tables: Vec<GeneratorTableDefinition>,
#[serde(skip)]
table_map: HashMap<String, usize>,
}
impl GeneratorSchema {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, SchemaError> {
let content = fs::read_to_string(path)?;
Self::from_yaml(&content)
}
pub fn from_yaml(yaml: &str) -> Result<Self, SchemaError> {
let mut schema: GeneratorSchema = serde_yaml::from_str(yaml)?;
schema.build_table_map();
Ok(schema)
}
fn build_table_map(&mut self) {
self.table_map = self
.tables
.iter()
.enumerate()
.map(|(idx, table)| (table.name.clone(), idx))
.collect();
}
pub fn get_table(&self, name: &str) -> Option<&GeneratorTableDefinition> {
self.table_map
.get(name)
.and_then(|&idx| self.tables.get(idx))
}
pub fn get_field_type(&self, table: &str, field: &str) -> Result<&Type, SchemaError> {
let table_schema = self
.get_table(table)
.ok_or_else(|| SchemaError::TableNotFound(table.to_string()))?;
table_schema
.get_field(field)
.map(|f| &f.field_type)
.ok_or_else(|| SchemaError::FieldNotFound {
table: table.to_string(),
field: field.to_string(),
})
}
pub fn table_names(&self) -> Vec<&str> {
self.tables.iter().map(|t| t.name.as_str()).collect()
}
pub fn to_database_schema(&self) -> DatabaseSchema {
DatabaseSchema::new(
self.tables
.iter()
.map(|t| t.to_table_definition())
.collect(),
)
}
}
pub type Schema = GeneratorSchema;
pub type TableDefinitionWithGenerators = GeneratorTableDefinition;
pub type FieldDefinition = GeneratorFieldDefinition;
pub type IDDefinition = GeneratorIDDefinition;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_column_definition_serde() {
let col = ColumnDefinition::new("email", Type::VarChar { length: 255 });
let yaml = serde_yaml::to_string(&col).unwrap();
let parsed: ColumnDefinition = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(col.name, parsed.name);
assert_eq!(col.column_type, parsed.column_type);
assert_eq!(col.nullable, parsed.nullable);
}
#[test]
fn test_table_definition_get_column() {
let table = TableDefinition::new(
"users",
ColumnDefinition::new("id", Type::Int64),
vec![
ColumnDefinition::new("email", Type::VarChar { length: 255 }),
ColumnDefinition::nullable("bio", Type::Text),
],
);
let id_col = table.get_column("id").expect("id should exist");
assert_eq!(id_col.column_type, Type::Int64);
let email_col = table.get_column("email").expect("email should exist");
assert_eq!(email_col.column_type, Type::VarChar { length: 255 });
assert!(table.get_column("nonexistent").is_none());
let names = table.column_names();
assert!(names.contains(&"id"));
assert!(names.contains(&"email"));
assert!(names.contains(&"bio"));
}
#[test]
fn test_database_schema_get_table() {
let schema = DatabaseSchema::new(vec![
TableDefinition::new(
"users",
ColumnDefinition::new("id", Type::Uuid),
vec![ColumnDefinition::new("name", Type::Text)],
),
TableDefinition::new(
"posts",
ColumnDefinition::new("id", Type::Int64),
vec![ColumnDefinition::new("title", Type::Text)],
),
]);
let users = schema.get_table("users").expect("users should exist");
assert_eq!(users.primary_key.column_type, Type::Uuid);
let posts = schema.get_table("posts").expect("posts should exist");
assert_eq!(posts.primary_key.column_type, Type::Int64);
assert!(schema.get_table("nonexistent").is_none());
let col_type = schema.get_column_type("users", "name").unwrap();
assert_eq!(col_type, &Type::Text);
}
const SAMPLE_SCHEMA: &str = r#"
version: 1
tables:
- name: users
id:
type: uuid
generator:
type: uuid_v4
fields:
- name: email
type:
type: var_char
length: 255
generator:
type: pattern
pattern: "user_{index}@example.com"
- name: age
type: int
generator:
type: int_range
min: 18
max: 80
- name: is_active
type:
type: tiny_int
width: 1
generator:
type: weighted_bool
true_weight: 0.8
"#;
#[test]
fn test_parse_generator_schema() {
let schema = GeneratorSchema::from_yaml(SAMPLE_SCHEMA).unwrap();
assert_eq!(schema.version, 1);
assert_eq!(schema.tables.len(), 1);
let users = schema.get_table("users").unwrap();
assert_eq!(users.name, "users");
assert_eq!(users.id.id_type, Type::Uuid);
assert_eq!(users.fields.len(), 3);
}
#[test]
fn test_get_field_type() {
let schema = GeneratorSchema::from_yaml(SAMPLE_SCHEMA).unwrap();
let email_type = schema.get_field_type("users", "email").unwrap();
assert_eq!(email_type, &Type::VarChar { length: 255 });
let age_type = schema.get_field_type("users", "age").unwrap();
assert_eq!(age_type, &Type::Int32);
}
#[test]
fn test_table_not_found() {
let schema = GeneratorSchema::from_yaml(SAMPLE_SCHEMA).unwrap();
let result = schema.get_field_type("nonexistent", "field");
assert!(matches!(result, Err(SchemaError::TableNotFound(_))));
}
#[test]
fn test_field_not_found() {
let schema = GeneratorSchema::from_yaml(SAMPLE_SCHEMA).unwrap();
let result = schema.get_field_type("users", "nonexistent");
assert!(matches!(result, Err(SchemaError::FieldNotFound { .. })));
}
#[test]
fn test_generator_configs() {
let yaml = r#"
version: 1
tables:
- name: test
id:
type: int
generator:
type: sequential
start: 1
fields:
- name: score
type: double
generator:
type: float_range
min: 0.0
max: 100.0
- name: status
type: text
generator:
type: one_of
values: ["active", "inactive", "pending"]
- name: metadata
type: json
generator:
type: static
value: { "version": 1 }
"#;
let schema = GeneratorSchema::from_yaml(yaml).unwrap();
let table = schema.get_table("test").unwrap();
assert!(matches!(
table.id.generator,
GeneratorConfig::Sequential { start: 1 }
));
let score_field = table.get_field("score").unwrap();
assert!(matches!(
score_field.generator,
GeneratorConfig::FloatRange {
min: 0.0,
max: 100.0
}
));
}
#[test]
fn test_generator_field_definition_flatten_serde() {
let field = GeneratorFieldDefinition {
name: "email".to_string(),
field_type: Type::VarChar { length: 255 },
generator: GeneratorConfig::Pattern {
pattern: "user_{index}@test.com".to_string(),
},
nullable: false,
};
let yaml = serde_yaml::to_string(&field).unwrap();
let parsed: GeneratorFieldDefinition = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(field.name, parsed.name);
assert_eq!(field.field_type, parsed.field_type);
assert_eq!(field.nullable, parsed.nullable);
let col = field.to_column_definition();
assert_eq!(col.name, "email");
assert_eq!(col.column_type, Type::VarChar { length: 255 });
assert!(!col.nullable);
}
#[test]
fn test_generator_schema_yaml_compatibility() {
let schema = GeneratorSchema::from_yaml(SAMPLE_SCHEMA).unwrap();
let db_schema = schema.to_database_schema();
assert_eq!(db_schema.tables.len(), 1);
let users = db_schema.get_table("users").expect("users should exist");
assert_eq!(users.primary_key.column_type, Type::Uuid);
assert_eq!(users.columns.len(), 3);
let email_type = db_schema.get_column_type("users", "email").unwrap();
assert_eq!(email_type, &Type::VarChar { length: 255 });
}
#[test]
fn test_type_alias_compatibility() {
let schema: Schema = GeneratorSchema::from_yaml(SAMPLE_SCHEMA).unwrap();
let _table: &TableDefinitionWithGenerators = schema.get_table("users").unwrap();
let _field: &FieldDefinition = schema
.get_table("users")
.unwrap()
.get_field("email")
.unwrap();
let _id: &IDDefinition = &schema.get_table("users").unwrap().id;
}
}