#![allow(clippy::result_large_err)]
use pandrs::schema_evolution::{
load_migration, load_schema, migration_to_json, migration_to_yaml, save_migration, save_schema,
schema_from_json, schema_from_yaml, schema_to_json, schema_to_yaml,
};
use pandrs::schema_evolution::{
BreakingChange, ColumnSchema, CompatibilityReport, DataFrameSchema, DefaultValue, Migration,
MigrationBuilder, SchemaChange, SchemaConstraint, SchemaDataType, SchemaFormat, SchemaMigrator,
SchemaRegistry, SchemaVersion, ValidationErrorType, ValidationReport,
};
use pandrs::{DataFrame, Series};
use std::env;
use std::fs;
fn make_df_users() -> DataFrame {
let mut df = DataFrame::new();
df.add_column(
"id".to_string(),
Series::new(vec![1i64, 2, 3], Some("id".to_string())).expect("series"),
)
.expect("add id");
df.add_column(
"name".to_string(),
Series::new(
vec!["Alice".to_string(), "Bob".to_string(), "Carol".to_string()],
Some("name".to_string()),
)
.expect("series"),
)
.expect("add name");
df
}
fn make_schema_v1() -> DataFrameSchema {
DataFrameSchema::new("users", SchemaVersion::new(1, 0, 0))
.with_column(ColumnSchema::new("id", SchemaDataType::Int64).with_nullable(false))
.with_column(ColumnSchema::new("name", SchemaDataType::String))
.with_constraint(SchemaConstraint::NotNull("id".to_string()))
}
fn make_schema_v2() -> DataFrameSchema {
DataFrameSchema::new("users", SchemaVersion::new(1, 1, 0))
.with_column(ColumnSchema::new("id", SchemaDataType::Int64).with_nullable(false))
.with_column(ColumnSchema::new("name", SchemaDataType::String))
.with_column(
ColumnSchema::new("email", SchemaDataType::String)
.with_nullable(true)
.with_default(DefaultValue::Str(String::new())),
)
.with_constraint(SchemaConstraint::NotNull("id".to_string()))
}
fn make_migration_v1_v2() -> Migration {
MigrationBuilder::new(
"m_users_001",
SchemaVersion::new(1, 0, 0),
SchemaVersion::new(1, 1, 0),
)
.description("Add email column")
.author("test-suite")
.add_column(
ColumnSchema::new("email", SchemaDataType::String)
.with_nullable(true)
.with_default(DefaultValue::Str(String::new())),
None,
)
.build()
}
#[test]
fn test_schema_version_ordering() {
let v1 = SchemaVersion::new(1, 0, 0);
let v2 = SchemaVersion::new(1, 1, 0);
let v3 = SchemaVersion::new(2, 0, 0);
assert!(v1 < v2);
assert!(v2 < v3);
assert_eq!(v1.to_string(), "1.0.0");
assert_eq!(v2.to_string(), "1.1.0");
}
#[test]
fn test_schema_version_next() {
let v = SchemaVersion::new(1, 2, 3);
assert_eq!(v.next_major(), SchemaVersion::new(2, 0, 0));
assert_eq!(v.next_minor(), SchemaVersion::new(1, 3, 0));
assert_eq!(v.next_patch(), SchemaVersion::new(1, 2, 4));
}
#[test]
fn test_schema_version_compatibility() {
let v1 = SchemaVersion::new(1, 0, 0);
let v2 = SchemaVersion::new(1, 5, 3);
let v3 = SchemaVersion::new(2, 0, 0);
assert!(v1.is_compatible_with(&v2));
assert!(!v1.is_compatible_with(&v3));
}
#[test]
fn test_column_schema_builder() {
let col = ColumnSchema::new("age", SchemaDataType::Int64)
.with_nullable(false)
.with_default(DefaultValue::Int(0))
.with_description("User age in years")
.with_tag("pii")
.with_tag("numeric");
assert_eq!(col.name, "age");
assert!(!col.nullable);
assert!(matches!(col.default_value, Some(DefaultValue::Int(0))));
assert_eq!(col.description.as_deref(), Some("User age in years"));
assert_eq!(col.tags, vec!["pii", "numeric"]);
}
#[test]
fn test_dataframe_schema_structure() {
let schema = make_schema_v1();
assert_eq!(schema.name, "users");
assert_eq!(schema.version, SchemaVersion::new(1, 0, 0));
assert_eq!(schema.columns.len(), 2);
assert_eq!(schema.constraints.len(), 1);
assert!(schema.has_column("id"));
assert!(schema.has_column("name"));
assert!(!schema.has_column("email"));
let id_col = schema.get_column("id").expect("id column");
assert_eq!(id_col.data_type, SchemaDataType::Int64);
assert!(!id_col.nullable);
}
#[test]
fn test_schema_constraint_ids() {
let c1 = SchemaConstraint::NotNull("col".to_string());
let c2 = SchemaConstraint::Unique(vec!["a".to_string(), "b".to_string()]);
let c3 = SchemaConstraint::Range {
col: "age".to_string(),
min: Some(0.0),
max: Some(120.0),
};
assert_eq!(c1.generate_id(), "notnull_col");
assert_eq!(c2.generate_id(), "unique_a_b");
assert!(c3.generate_id().starts_with("range_age"));
}
#[test]
fn test_schema_json_round_trip() {
let schema = make_schema_v1();
let json = schema_to_json(&schema).expect("to json");
assert!(json.contains("users"));
assert!(json.contains("Int64"));
let recovered = schema_from_json(&json).expect("from json");
assert_eq!(recovered.name, schema.name);
assert_eq!(recovered.version, schema.version);
assert_eq!(recovered.columns.len(), schema.columns.len());
assert_eq!(recovered.constraints.len(), schema.constraints.len());
assert_eq!(recovered.column_names(), schema.column_names());
}
#[test]
fn test_schema_yaml_round_trip() {
let schema = make_schema_v2();
let yaml = schema_to_yaml(&schema).expect("to yaml");
assert!(yaml.contains("users"));
let recovered = schema_from_yaml(&yaml).expect("from yaml");
assert_eq!(recovered.name, schema.name);
assert_eq!(recovered.version, schema.version);
assert_eq!(recovered.columns.len(), 3);
}
#[test]
fn test_migration_json_round_trip() {
let migration = make_migration_v1_v2();
let json = migration_to_json(&migration).expect("to json");
assert!(json.contains("m_users_001"));
let recovered: Migration =
pandrs::schema_evolution::migration_from_json(&json).expect("from json");
assert_eq!(recovered.id, migration.id);
assert_eq!(recovered.from_version, migration.from_version);
assert_eq!(recovered.to_version, migration.to_version);
assert_eq!(recovered.changes.len(), migration.changes.len());
}
#[test]
fn test_migration_yaml_round_trip() {
let migration = make_migration_v1_v2();
let yaml = migration_to_yaml(&migration).expect("to yaml");
let recovered: Migration =
pandrs::schema_evolution::migration_from_yaml(&yaml).expect("from yaml");
assert_eq!(recovered.id, migration.id);
assert_eq!(recovered.changes.len(), migration.changes.len());
}
#[test]
fn test_save_load_schema_json() {
let schema = make_schema_v1();
let dir = env::temp_dir();
let path = dir.join("pandrs_test_schema_json.json");
let path_str = path.to_str().expect("path");
save_schema(&schema, path_str, SchemaFormat::Json).expect("save");
let loaded = load_schema(path_str).expect("load");
assert_eq!(loaded.name, schema.name);
assert_eq!(loaded.version, schema.version);
assert_eq!(loaded.columns.len(), schema.columns.len());
let _ = fs::remove_file(&path);
}
#[test]
fn test_save_load_schema_yaml() {
let schema = make_schema_v2();
let dir = env::temp_dir();
let path = dir.join("pandrs_test_schema_yaml.yaml");
let path_str = path.to_str().expect("path");
save_schema(&schema, path_str, SchemaFormat::Yaml).expect("save");
let loaded = load_schema(path_str).expect("load");
assert_eq!(loaded.name, schema.name);
assert_eq!(loaded.columns.len(), 3);
let _ = fs::remove_file(&path);
}
#[test]
fn test_save_load_migration_json() {
let migration = make_migration_v1_v2();
let dir = env::temp_dir();
let path = dir.join("pandrs_test_migration.json");
let path_str = path.to_str().expect("path");
save_migration(&migration, path_str, SchemaFormat::Json).expect("save");
let loaded = load_migration(path_str).expect("load");
assert_eq!(loaded.id, migration.id);
assert_eq!(loaded.changes.len(), migration.changes.len());
let _ = fs::remove_file(&path);
}
#[test]
fn test_apply_add_column_migration() {
let df = make_df_users();
let migration = make_migration_v1_v2();
let migrator = SchemaMigrator::empty();
let result = migrator.apply_migration(&df, &migration).expect("migrate");
assert!(result.contains_column("email"));
assert!(result.contains_column("id"));
assert!(result.contains_column("name"));
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_apply_rename_column_migration() {
let df = make_df_users();
let migration = MigrationBuilder::new(
"m002",
SchemaVersion::new(1, 0, 0),
SchemaVersion::new(1, 1, 0),
)
.rename_column("name", "full_name")
.build();
let migrator = SchemaMigrator::empty();
let result = migrator.apply_migration(&df, &migration).expect("migrate");
assert!(result.contains_column("full_name"));
assert!(!result.contains_column("name"));
assert!(result.contains_column("id"));
}
#[test]
fn test_apply_remove_column_migration() {
let df = make_df_users();
let migration = MigrationBuilder::new(
"m003",
SchemaVersion::new(1, 0, 0),
SchemaVersion::new(1, 1, 0),
)
.remove_column("name")
.build();
let migrator = SchemaMigrator::empty();
let result = migrator.apply_migration(&df, &migration).expect("migrate");
assert!(!result.contains_column("name"));
assert!(result.contains_column("id"));
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_apply_change_type_migration() {
let df = make_df_users();
let migration = MigrationBuilder::new(
"m004",
SchemaVersion::new(1, 0, 0),
SchemaVersion::new(1, 1, 0),
)
.change_type("id", SchemaDataType::String, None)
.build();
let migrator = SchemaMigrator::empty();
let result = migrator.apply_migration(&df, &migration).expect("migrate");
assert!(result.contains_column("id"));
let id_values = result.get_column_string_values("id").expect("id values");
assert_eq!(id_values.len(), 3);
}
#[test]
fn test_apply_multi_step_migration() {
let df = make_df_users();
let migration = MigrationBuilder::new(
"m005",
SchemaVersion::new(1, 0, 0),
SchemaVersion::new(2, 0, 0),
)
.add_column(
ColumnSchema::new("score", SchemaDataType::Float64).with_default(DefaultValue::Float(0.0)),
None,
)
.rename_column("name", "full_name")
.remove_column("id")
.build();
let migrator = SchemaMigrator::empty();
let result = migrator.apply_migration(&df, &migration).expect("migrate");
assert!(result.contains_column("score"));
assert!(result.contains_column("full_name"));
assert!(!result.contains_column("name"));
assert!(!result.contains_column("id"));
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_metadata_only_changes_no_data_loss() {
let df = make_df_users();
let migration = MigrationBuilder::new(
"m_meta",
SchemaVersion::new(1, 0, 0),
SchemaVersion::new(1, 0, 1),
)
.build();
let migration = Migration {
changes: vec![
SchemaChange::SetNullable {
column: "name".to_string(),
nullable: false,
},
SchemaChange::SetColumnDescription {
column: "id".to_string(),
description: "Primary key".to_string(),
},
SchemaChange::AddConstraint {
constraint: SchemaConstraint::NotNull("name".to_string()),
},
],
..migration
};
let migrator = SchemaMigrator::empty();
let result = migrator.apply_migration(&df, &migration).expect("migrate");
assert_eq!(result.column_names(), df.column_names());
assert_eq!(result.row_count(), df.row_count());
}
#[test]
fn test_validation_valid_dataframe() {
let df = make_df_users();
let schema = make_schema_v1();
let migrator = SchemaMigrator::empty();
let report = migrator.validate(&df, &schema).expect("validate");
assert!(report.is_valid, "errors: {:?}", report.errors);
assert!(report.errors.is_empty());
}
#[test]
fn test_validation_missing_column() {
let mut df = DataFrame::new();
df.add_column(
"id".to_string(),
Series::new(vec![1i64], Some("id".to_string())).expect("series"),
)
.expect("add");
let schema = make_schema_v1();
let migrator = SchemaMigrator::empty();
let report = migrator.validate(&df, &schema).expect("validate");
assert!(!report.is_valid);
assert!(report
.errors
.iter()
.any(|e| e.error_type == ValidationErrorType::MissingColumn && e.column == "name"));
}
#[test]
fn test_validation_extra_column_warning() {
let mut df = make_df_users();
df.add_column(
"extra".to_string(),
Series::new(
vec!["x".to_string(), "y".to_string(), "z".to_string()],
Some("extra".to_string()),
)
.expect("series"),
)
.expect("add");
let schema = make_schema_v1();
let migrator = SchemaMigrator::empty();
let report = migrator.validate(&df, &schema).expect("validate");
assert!(report.is_valid); assert!(report.warnings.iter().any(|w| w.contains("extra")));
}
#[test]
fn test_validation_range_constraint_violation() {
let mut df = DataFrame::new();
df.add_column(
"age".to_string(),
Series::new(vec![25.0f64, 200.0, 30.0], Some("age".to_string())).expect("series"),
)
.expect("add");
let schema = DataFrameSchema::new("test", SchemaVersion::initial())
.with_column(ColumnSchema::new("age", SchemaDataType::Float64))
.with_constraint(SchemaConstraint::Range {
col: "age".to_string(),
min: Some(0.0),
max: Some(150.0),
});
let migrator = SchemaMigrator::empty();
let report = migrator.validate(&df, &schema).expect("validate");
assert!(!report.is_valid);
assert!(report
.errors
.iter()
.any(|e| e.error_type == ValidationErrorType::RangeViolation));
}
#[test]
fn test_validation_regex_constraint() {
let mut df = DataFrame::new();
df.add_column(
"email".to_string(),
Series::new(
vec!["ok@test.com".to_string(), "not-an-email".to_string()],
Some("email".to_string()),
)
.expect("series"),
)
.expect("add");
let schema = DataFrameSchema::new("test", SchemaVersion::initial())
.with_column(ColumnSchema::new("email", SchemaDataType::String))
.with_constraint(SchemaConstraint::Regex {
col: "email".to_string(),
pattern: r"^[^@]+@[^@]+\.[^@]+$".to_string(),
});
let migrator = SchemaMigrator::empty();
let report = migrator.validate(&df, &schema).expect("validate");
assert!(!report.is_valid);
assert!(report
.errors
.iter()
.any(|e| e.error_type == ValidationErrorType::RegexViolation));
}
#[test]
fn test_validation_enum_constraint() {
let mut df = DataFrame::new();
df.add_column(
"status".to_string(),
Series::new(
vec!["active".to_string(), "deleted".to_string()],
Some("status".to_string()),
)
.expect("series"),
)
.expect("add");
let schema = DataFrameSchema::new("test", SchemaVersion::initial())
.with_column(ColumnSchema::new("status", SchemaDataType::String))
.with_constraint(SchemaConstraint::Enum {
col: "status".to_string(),
values: vec!["active".to_string(), "pending".to_string()],
});
let migrator = SchemaMigrator::empty();
let report = migrator.validate(&df, &schema).expect("validate");
assert!(!report.is_valid);
assert!(report
.errors
.iter()
.any(|e| e.error_type == ValidationErrorType::EnumViolation));
}
#[test]
fn test_validation_unique_constraint() {
let mut df = DataFrame::new();
df.add_column(
"code".to_string(),
Series::new(
vec!["A".to_string(), "B".to_string(), "A".to_string()],
Some("code".to_string()),
)
.expect("series"),
)
.expect("add");
let schema = DataFrameSchema::new("test", SchemaVersion::initial())
.with_column(ColumnSchema::new("code", SchemaDataType::String))
.with_constraint(SchemaConstraint::Unique(vec!["code".to_string()]));
let migrator = SchemaMigrator::empty();
let report = migrator.validate(&df, &schema).expect("validate");
assert!(!report.is_valid);
assert!(report
.errors
.iter()
.any(|e| e.error_type == ValidationErrorType::UniqueViolation));
}
#[test]
fn test_infer_schema_from_dataframe() {
let df = make_df_users();
let migrator = SchemaMigrator::empty();
let schema = migrator.infer_schema(&df, "inferred_users");
assert_eq!(schema.name, "inferred_users");
assert_eq!(schema.version, SchemaVersion::initial());
assert!(schema.has_column("id"));
assert!(schema.has_column("name"));
assert_eq!(schema.columns.len(), 2);
let id_col = schema.get_column("id").expect("id");
assert_eq!(id_col.data_type, SchemaDataType::Int64);
let name_col = schema.get_column("name").expect("name");
assert_eq!(name_col.data_type, SchemaDataType::String);
assert!(schema.metadata.contains_key("row_count"));
}
#[test]
fn test_infer_schema_float_column() {
let mut df = DataFrame::new();
df.add_column(
"score".to_string(),
Series::new(vec![1.5f64, 2.7, 3.15], Some("score".to_string())).expect("series"),
)
.expect("add");
let migrator = SchemaMigrator::empty();
let schema = migrator.infer_schema(&df, "test");
let col = schema.get_column("score").expect("score");
assert_eq!(col.data_type, SchemaDataType::Float64);
}
#[test]
fn test_compatibility_fully_compatible() {
let from = make_schema_v1();
let to = make_schema_v2();
let migrator = SchemaMigrator::empty();
let report = migrator.check_compatibility(&from, &to);
assert!(
report.is_compatible,
"breaking: {:?}",
report.breaking_changes
);
assert!(report.breaking_changes.is_empty());
}
#[test]
fn test_compatibility_missing_required_column() {
let from = DataFrameSchema::new("v1", SchemaVersion::new(1, 0, 0))
.with_column(ColumnSchema::new("id", SchemaDataType::Int64));
let to = DataFrameSchema::new("v2", SchemaVersion::new(2, 0, 0))
.with_column(ColumnSchema::new("id", SchemaDataType::Int64))
.with_column(ColumnSchema::new("mandatory", SchemaDataType::String).with_nullable(false));
let migrator = SchemaMigrator::empty();
let report = migrator.check_compatibility(&from, &to);
assert!(!report.is_compatible);
assert!(!report.breaking_changes.is_empty());
}
#[test]
fn test_compatibility_type_narrowing() {
let from = DataFrameSchema::new("v1", SchemaVersion::new(1, 0, 0))
.with_column(ColumnSchema::new("flag", SchemaDataType::Boolean));
let to =
DataFrameSchema::new("v2", SchemaVersion::new(2, 0, 0)).with_column(ColumnSchema::new(
"flag",
SchemaDataType::Categorical {
categories: vec!["yes".to_string(), "no".to_string()],
},
));
let migrator = SchemaMigrator::empty();
let report = migrator.check_compatibility(&from, &to);
assert!(!report.is_compatible);
}
#[test]
fn test_compatibility_widening_type() {
let from = DataFrameSchema::new("v1", SchemaVersion::new(1, 0, 0))
.with_column(ColumnSchema::new("count", SchemaDataType::Int64));
let to = DataFrameSchema::new("v2", SchemaVersion::new(1, 1, 0))
.with_column(ColumnSchema::new("count", SchemaDataType::Float64));
let migrator = SchemaMigrator::empty();
let report = migrator.check_compatibility(&from, &to);
assert!(report.is_compatible);
assert!(!report.non_breaking_changes.is_empty());
}
#[test]
fn test_compatibility_dropped_column_noted() {
let from = DataFrameSchema::new("v1", SchemaVersion::new(1, 0, 0))
.with_column(ColumnSchema::new("id", SchemaDataType::Int64))
.with_column(ColumnSchema::new("legacy", SchemaDataType::String));
let to = DataFrameSchema::new("v2", SchemaVersion::new(2, 0, 0))
.with_column(ColumnSchema::new("id", SchemaDataType::Int64));
let migrator = SchemaMigrator::empty();
let report = migrator.check_compatibility(&from, &to);
assert!(report.is_compatible);
assert!(report
.non_breaking_changes
.iter()
.any(|c| c.contains("legacy")));
}
#[test]
fn test_registry_register_and_retrieve() {
let mut registry = SchemaRegistry::new();
registry.register(make_schema_v1()).expect("register v1");
registry.register(make_schema_v2()).expect("register v2");
let latest = registry.get_latest("users").expect("latest");
assert_eq!(latest.version, SchemaVersion::new(1, 1, 0));
let v1 = registry.get_version("users", &SchemaVersion::new(1, 0, 0));
assert!(v1.is_some());
let versions = registry.versions_of("users");
assert_eq!(versions.len(), 2);
}
#[test]
fn test_registry_duplicate_version_error() {
let mut registry = SchemaRegistry::new();
registry.register(make_schema_v1()).expect("first register");
let err = registry.register(make_schema_v1());
assert!(err.is_err());
}
#[test]
fn test_registry_migration_path_single_hop() {
let mut registry = SchemaRegistry::new();
registry.register(make_schema_v1()).expect("v1");
registry.register(make_schema_v2()).expect("v2");
registry
.add_migration_for_schema("users", make_migration_v1_v2())
.expect("add migration");
let path = registry
.find_migration_path(
"users",
&SchemaVersion::new(1, 0, 0),
&SchemaVersion::new(1, 1, 0),
)
.expect("path");
assert_eq!(path.len(), 1);
assert_eq!(path[0].id, "m_users_001");
}
#[test]
fn test_registry_migration_path_multi_hop() {
let mut registry = SchemaRegistry::new();
let v1 = DataFrameSchema::new("products", SchemaVersion::new(1, 0, 0))
.with_column(ColumnSchema::new("id", SchemaDataType::Int64));
let v2 = DataFrameSchema::new("products", SchemaVersion::new(1, 1, 0))
.with_column(ColumnSchema::new("id", SchemaDataType::Int64))
.with_column(ColumnSchema::new("name", SchemaDataType::String));
let v3 = DataFrameSchema::new("products", SchemaVersion::new(1, 2, 0))
.with_column(ColumnSchema::new("id", SchemaDataType::Int64))
.with_column(ColumnSchema::new("name", SchemaDataType::String))
.with_column(ColumnSchema::new("price", SchemaDataType::Float64));
registry.register(v1).expect("v1");
registry.register(v2).expect("v2");
registry.register(v3).expect("v3");
let m1 = MigrationBuilder::new(
"p_m001",
SchemaVersion::new(1, 0, 0),
SchemaVersion::new(1, 1, 0),
)
.add_column(ColumnSchema::new("name", SchemaDataType::String), None)
.build();
let m2 = MigrationBuilder::new(
"p_m002",
SchemaVersion::new(1, 1, 0),
SchemaVersion::new(1, 2, 0),
)
.add_column(
ColumnSchema::new("price", SchemaDataType::Float64).with_default(DefaultValue::Float(0.0)),
None,
)
.build();
registry
.add_migration_for_schema("products", m1)
.expect("m1");
registry
.add_migration_for_schema("products", m2)
.expect("m2");
let path = registry
.find_migration_path(
"products",
&SchemaVersion::new(1, 0, 0),
&SchemaVersion::new(1, 2, 0),
)
.expect("path");
assert_eq!(path.len(), 2);
}
#[test]
fn test_registry_no_path_error() {
let registry = SchemaRegistry::new();
let err = registry.find_migration_path(
"nonexistent",
&SchemaVersion::new(1, 0, 0),
&SchemaVersion::new(2, 0, 0),
);
assert!(err.is_err());
}
#[test]
fn test_registry_same_version_path_empty() {
let registry = SchemaRegistry::new();
let path = registry
.find_migration_path(
"any",
&SchemaVersion::new(1, 0, 0),
&SchemaVersion::new(1, 0, 0),
)
.expect("same version");
assert!(path.is_empty());
}
#[test]
fn test_end_to_end_migrate_via_registry() {
let mut registry = SchemaRegistry::new();
registry.register(make_schema_v1()).expect("v1");
registry.register(make_schema_v2()).expect("v2");
registry
.add_migration_for_schema("users", make_migration_v1_v2())
.expect("add migration");
let migrator = SchemaMigrator::new(registry);
let df = make_df_users();
let result = migrator
.migrate(
&df,
"users",
&SchemaVersion::new(1, 0, 0),
&SchemaVersion::new(1, 1, 0),
)
.expect("migrate");
assert!(result.contains_column("email"));
assert!(result.contains_column("id"));
assert!(result.contains_column("name"));
assert_eq!(result.row_count(), 3);
let target_schema = make_schema_v2();
let report = migrator.registry.get_latest("users").map(|s| {
let m = SchemaMigrator::empty();
m.validate(&result, s).expect("validate")
});
if let Some(report) = report {
assert!(report.is_valid, "errors: {:?}", report.errors);
}
}
#[test]
fn test_end_to_end_schema_bundle_save_load() {
use pandrs::schema_evolution::{load_bundle, save_bundle, SchemaBundle};
let bundle = SchemaBundle::new()
.with_schema(make_schema_v1())
.with_schema(make_schema_v2())
.with_migration(make_migration_v1_v2());
let dir = env::temp_dir();
let path = dir.join("pandrs_test_bundle.json");
let path_str = path.to_str().expect("path");
save_bundle(&bundle, path_str, SchemaFormat::Json).expect("save bundle");
let loaded = load_bundle(path_str).expect("load bundle");
assert_eq!(loaded.schemas.len(), 2);
assert_eq!(loaded.migrations.len(), 1);
assert_eq!(loaded.schemas[0].name, "users");
let _ = fs::remove_file(&path);
}
#[test]
fn test_schema_change_breaking_detection() {
let rm = SchemaChange::RemoveColumn {
name: "col".to_string(),
};
let add = SchemaChange::AddColumn {
schema: ColumnSchema::new("new", SchemaDataType::String),
position: None,
};
let rename = SchemaChange::RenameColumn {
from: "a".to_string(),
to: "b".to_string(),
};
let change_type = SchemaChange::ChangeType {
column: "x".to_string(),
new_type: SchemaDataType::Int64,
converter: None,
};
let make_non_nullable = SchemaChange::SetNullable {
column: "y".to_string(),
nullable: false,
};
let make_nullable = SchemaChange::SetNullable {
column: "y".to_string(),
nullable: true,
};
assert!(rm.is_breaking());
assert!(!add.is_breaking());
assert!(!rename.is_breaking());
assert!(change_type.is_breaking());
assert!(make_non_nullable.is_breaking());
assert!(!make_nullable.is_breaking());
}
#[test]
fn test_migration_breaking_changes_summary() {
let migration = MigrationBuilder::new(
"breaking_m",
SchemaVersion::new(1, 0, 0),
SchemaVersion::new(2, 0, 0),
)
.remove_column("legacy_field")
.add_column(ColumnSchema::new("new_field", SchemaDataType::String), None)
.change_type("id", SchemaDataType::String, None)
.build();
assert!(migration.has_breaking_changes());
let breaking = migration.breaking_changes();
assert_eq!(breaking.len(), 2); }
#[test]
fn test_add_column_with_bool_default() {
let df = make_df_users();
let migrator = SchemaMigrator::empty();
let migration = MigrationBuilder::new(
"bool_default",
SchemaVersion::new(1, 0, 0),
SchemaVersion::new(1, 1, 0),
)
.add_column(
ColumnSchema::new("active", SchemaDataType::Boolean).with_default(DefaultValue::Bool(true)),
None,
)
.build();
let result = migrator.apply_migration(&df, &migration).expect("apply");
assert!(result.contains_column("active"));
let vals = result.get_column_numeric_values("active").expect("numeric");
assert!(vals.iter().all(|&v| v == 1.0)); }
#[test]
fn test_add_column_with_string_default() {
let df = make_df_users();
let migrator = SchemaMigrator::empty();
let migration = MigrationBuilder::new(
"str_default",
SchemaVersion::new(1, 0, 0),
SchemaVersion::new(1, 1, 0),
)
.add_column(
ColumnSchema::new("department", SchemaDataType::String)
.with_default(DefaultValue::Str("engineering".to_string())),
None,
)
.build();
let result = migrator.apply_migration(&df, &migration).expect("apply");
assert!(result.contains_column("department"));
let vals = result
.get_column_string_values("department")
.expect("values");
assert!(vals.iter().all(|v| v == "engineering"));
}