pub mod filter;
#[cfg(feature = "surrealdb")]
pub mod import;
#[cfg(feature = "sql")]
pub mod sql;
#[cfg(feature = "surrealdb")]
pub mod surql;
pub mod types;
pub use crate::schemasync::mockmake::MockGenerationConfig;
#[cfg(feature = "surrealdb")]
pub use import::SchemaImporter;
#[cfg(feature = "surrealdb")]
pub use surql::SurrealdbComparator;
pub use types::{
AccessDefinition, FieldDefinition, ObjectType, PermissionSet, SchemaDefinition, SchemaType,
TableDefinition,
};
#[cfg(feature = "surrealdb")]
use crate::schemasync::config::{PerformanceConfig, SchemasyncMockGenConfig};
use crate::{
EvenframeError, Result,
schemasync::TableConfig,
types::{FieldType, TaggedUnion, VariantData},
};
#[cfg(feature = "surrealdb")]
use ::surrealdb::Surreal;
#[cfg(feature = "surrealdb")]
use ::surrealdb::engine::remote::http::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::HashSet;
#[cfg(feature = "schemasync")]
use crate::schemasync::database::types::SchemaExport;
#[cfg(feature = "schemasync")]
#[async_trait::async_trait]
pub trait SchemaComparator: Send + Sync {
async fn compare_schemas(
&self,
tables: &HashMap<String, TableConfig>,
objects: &HashMap<String, crate::types::StructConfig>,
enums: &HashMap<String, TaggedUnion>,
) -> Result<SchemaChanges>;
async fn get_current_schema(&self) -> Result<SchemaExport>;
fn supports_embedded_comparison(&self) -> bool;
}
#[cfg(feature = "schemasync")]
pub fn create_comparator<'a>(
_provider: &'a dyn crate::schemasync::database::DatabaseProvider,
) -> Box<dyn SchemaComparator + 'a> {
#[cfg(feature = "sql")]
{
Box::new(sql::SqlSchemaComparator::new(_provider))
}
#[cfg(not(feature = "sql"))]
{
panic!("No SQL feature enabled - use SurrealdbComparator directly for SurrealDB")
}
}
pub struct Comparator;
pub use super::PreservationMode;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AccessChangeType {
JwtKeyChanged,
IssuerKeyChanged,
JwtUrlChanged,
AuthenticateClauseChanged,
DurationChanged,
SigninChanged,
SignupChanged,
OtherChange(String),
}
impl AccessChangeType {
pub fn is_ignorable(&self) -> bool {
matches!(
self,
AccessChangeType::JwtKeyChanged | AccessChangeType::IssuerKeyChanged
)
}
}
impl std::fmt::Display for AccessChangeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AccessChangeType::JwtKeyChanged => write!(f, "JWT key changed"),
AccessChangeType::IssuerKeyChanged => write!(f, "Issuer key changed"),
AccessChangeType::JwtUrlChanged => write!(f, "JWT URL changed"),
AccessChangeType::AuthenticateClauseChanged => write!(f, "Authenticate clause changed"),
AccessChangeType::DurationChanged => write!(f, "EvenframeDuration changed"),
AccessChangeType::SigninChanged => write!(f, "Signin changed"),
AccessChangeType::SignupChanged => write!(f, "Signup changed"),
AccessChangeType::OtherChange(msg) => write!(f, "{}", msg),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaChanges {
pub new_tables: Vec<String>,
pub removed_tables: Vec<String>,
pub modified_tables: Vec<TableChanges>,
pub new_accesses: Vec<String>,
pub removed_accesses: Vec<String>,
pub modified_accesses: Vec<AccessChange>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessChange {
pub access_name: String,
pub changes: Vec<AccessChangeType>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableChanges {
pub table_name: String,
pub new_fields: Vec<String>,
pub removed_fields: Vec<String>,
pub modified_fields: Vec<FieldChange>,
pub permission_changed: bool,
pub schema_type_changed: bool,
pub new_events: Vec<String>,
pub removed_events: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ChangeType {
Added,
Removed,
Modified,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldChange {
pub field_name: String,
pub old_type: String,
pub new_type: String,
pub change_type: ChangeType,
pub required_changed: bool,
pub default_changed: bool,
}
impl SchemaChanges {
pub fn is_field_unchanged(&self, table: &str, field: &str) -> bool {
if self.new_tables.contains(&table.to_string())
|| self.removed_tables.contains(&table.to_string())
{
return false;
}
for table_change in &self.modified_tables {
if table_change.table_name == table {
if table_change.new_fields.contains(&field.to_string())
|| table_change.removed_fields.contains(&field.to_string())
{
return false;
}
for field_change in &table_change.modified_fields {
if field_change.field_name == field {
return false;
}
}
return true;
}
}
true
}
pub fn get_fields_needing_generation(&self, table: &str) -> Vec<String> {
let mut fields = Vec::new();
if self.new_tables.contains(&table.to_string()) {
return vec!["*".to_string()]; }
for table_change in &self.modified_tables {
if table_change.table_name == table {
fields.extend(table_change.new_fields.clone());
for field_change in &table_change.modified_fields {
fields.push(field_change.field_name.clone());
}
}
}
fields
}
pub fn summary(&self) -> String {
let mut summary = Vec::new();
if !self.new_tables.is_empty() {
summary.push(format!("New tables: {}", self.new_tables.join(", ")));
}
if !self.removed_tables.is_empty() {
summary.push(format!(
"Removed tables: {}",
self.removed_tables.join(", ")
));
}
if !self.modified_tables.is_empty() {
summary.push(format!(
"Modified tables: {}",
self.modified_tables
.iter()
.map(|t| t.table_name.clone())
.collect::<Vec<_>>()
.join(", ")
));
}
for table in &self.modified_tables {
if !table.new_events.is_empty() {
summary.push(format!(
"New events on {}: {}",
table.table_name,
table.new_events.join(", ")
));
}
if !table.removed_events.is_empty() {
summary.push(format!(
"Removed events on {}: {}",
table.table_name,
table.removed_events.join(", ")
));
}
}
if !self.new_accesses.is_empty() {
summary.push(format!("New accesses: {}", self.new_accesses.join(", ")));
}
if !self.removed_accesses.is_empty() {
summary.push(format!(
"Removed accesses: {}",
self.removed_accesses.join(", ")
));
}
if !self.modified_accesses.is_empty() {
summary.push(format!(
"Modified accesses: {}",
self.modified_accesses
.iter()
.map(|a| a.access_name.clone())
.collect::<Vec<_>>()
.join(", ")
));
}
if summary.is_empty() {
"No changes detected".to_string()
} else {
summary.join("\n")
}
}
}
impl Comparator {
pub fn compare(old: &SchemaDefinition, new: &SchemaDefinition) -> Result<SchemaChanges> {
tracing::debug!("Starting detailed schema comparison");
let mut changes = SchemaChanges {
new_tables: Vec::new(),
removed_tables: Vec::new(),
modified_tables: Vec::new(),
new_accesses: Vec::new(),
removed_accesses: Vec::new(),
modified_accesses: Vec::new(),
};
let old_tables: HashSet<String> = old.tables.keys().cloned().collect();
let new_tables: HashSet<String> = new.tables.keys().cloned().collect();
tracing::trace!(
old_table_count = old_tables.len(),
new_table_count = new_tables.len(),
"Comparing table sets"
);
for table in new_tables.difference(&old_tables) {
tracing::trace!(table = %table, "Found new table");
changes.new_tables.push(table.clone());
}
for table in old_tables.difference(&new_tables) {
tracing::trace!(table = %table, "Found removed table");
changes.removed_tables.push(table.clone());
}
for table in old_tables.intersection(&new_tables) {
tracing::trace!(table = %table, "Comparing table");
let (old_table, new_table) = match (old.tables.get(table), new.tables.get(table)) {
(Some(o), Some(n)) => (o, n),
_ => {
let msg =
format!("Table '{table}' present in key set but missing in schema maps");
tracing::error!(table = %table, "{}", msg);
return Err(EvenframeError::comparison(msg));
}
};
if let Some(table_changes) = Self::compare_tables(table, old_table, new_table)? {
tracing::trace!(
table = %table,
new_fields = table_changes.new_fields.len(),
removed_fields = table_changes.removed_fields.len(),
modified_fields = table_changes.modified_fields.len(),
"Table has changes"
);
changes.modified_tables.push(table_changes);
}
}
let old_edges: HashSet<String> = old.edges.keys().cloned().collect();
let new_edges: HashSet<String> = new.edges.keys().cloned().collect();
for edge in new_edges.difference(&old_edges) {
changes.new_tables.push(edge.clone());
}
for edge in old_edges.difference(&new_edges) {
changes.removed_tables.push(edge.clone());
}
for edge in old_edges.intersection(&new_edges) {
let (old_edge, new_edge) = match (old.edges.get(edge), new.edges.get(edge)) {
(Some(o), Some(n)) => (o, n),
_ => {
let msg =
format!("Edge '{edge}' present in key set but missing in schema maps");
tracing::error!(edge = %edge, "{}", msg);
return Err(EvenframeError::comparison(msg));
}
};
if let Some(edge_changes) = Self::compare_tables(edge, old_edge, new_edge)? {
changes.modified_tables.push(edge_changes);
}
}
let old_access_names: HashSet<String> =
old.accesses.iter().map(|a| a.name.clone()).collect();
let new_access_names: HashSet<String> =
new.accesses.iter().map(|a| a.name.clone()).collect();
for access_name in new_access_names.difference(&old_access_names) {
changes.new_accesses.push(access_name.clone());
}
for access_name in old_access_names.difference(&new_access_names) {
changes.removed_accesses.push(access_name.clone());
}
for access_name in old_access_names.intersection(&new_access_names) {
let old_access = match old.accesses.iter().find(|a| &a.name == access_name) {
Some(a) => a,
None => {
let msg = format!(
"Access '{access_name}' present in key set but missing in old schema list"
);
tracing::error!(access = %access_name, "{}", msg);
return Err(EvenframeError::comparison(msg));
}
};
let new_access = match new.accesses.iter().find(|a| &a.name == access_name) {
Some(a) => a,
None => {
let msg = format!(
"Access '{access_name}' present in key set but missing in new schema list"
);
tracing::error!(access = %access_name, "{}", msg);
return Err(EvenframeError::comparison(msg));
}
};
if let Some(access_change) = Self::compare_accesses(old_access, new_access) {
changes.modified_accesses.push(access_change);
}
}
tracing::debug!(
new_tables = changes.new_tables.len(),
removed_tables = changes.removed_tables.len(),
modified_tables = changes.modified_tables.len(),
new_accesses = changes.new_accesses.len(),
removed_accesses = changes.removed_accesses.len(),
modified_accesses = changes.modified_accesses.len(),
"Schema comparison complete"
);
Ok(changes)
}
fn compare_tables(
table_name: &str,
old_table: &TableDefinition,
new_table: &TableDefinition,
) -> Result<Option<TableChanges>> {
let mut table_changes = TableChanges {
table_name: table_name.to_string(),
new_fields: Vec::new(),
removed_fields: Vec::new(),
modified_fields: Vec::new(),
permission_changed: false,
schema_type_changed: false,
new_events: Vec::new(),
removed_events: Vec::new(),
};
if old_table.schema_type != new_table.schema_type {
table_changes.schema_type_changed = true;
}
if old_table.permissions != new_table.permissions {
table_changes.permission_changed = true;
}
let old_fields: HashSet<String> = old_table.fields.keys().cloned().collect();
let new_fields: HashSet<String> = new_table.fields.keys().cloned().collect();
for field in new_fields.difference(&old_fields) {
table_changes.new_fields.push(field.clone());
}
for field in old_fields.difference(&new_fields) {
table_changes.removed_fields.push(field.clone());
}
for field in old_fields.intersection(&new_fields) {
let old_field = match old_table.fields.get(field) {
Some(f) => f,
None => {
let msg = format!(
"Field '{field}' present in old/new intersection but missing in old_table '{}'",
table_name
);
tracing::error!(table = %table_name, field = %field, "{}", msg);
return Err(EvenframeError::comparison(msg));
}
};
let new_field = match new_table.fields.get(field) {
Some(f) => f,
None => {
let msg = format!(
"Field '{field}' present in old/new intersection but missing in new_table '{}'",
table_name
);
tracing::error!(table = %table_name, field = %field, "{}", msg);
return Err(EvenframeError::comparison(msg));
}
};
if old_field.field_type != new_field.field_type {
table_changes.modified_fields.push(FieldChange {
field_name: field.to_string(),
old_type: old_field.field_type.to_string(),
new_type: new_field.field_type.to_string(),
change_type: ChangeType::Modified,
required_changed: false,
default_changed: false,
});
} else {
if let Some(field_change) = Self::compare_fields(field, old_field, new_field) {
table_changes.modified_fields.push(field_change);
}
}
}
let old_wildcard_fields: HashSet<String> =
old_table.array_wildcard_fields.keys().cloned().collect();
let new_wildcard_fields: HashSet<String> =
new_table.array_wildcard_fields.keys().cloned().collect();
for field in new_wildcard_fields.difference(&old_wildcard_fields) {
table_changes.new_fields.push(format!("{}[*]", field));
}
for field in old_wildcard_fields.difference(&new_wildcard_fields) {
table_changes.removed_fields.push(format!("{}[*]", field));
}
for field in old_wildcard_fields.intersection(&new_wildcard_fields) {
let old_wild = match old_table.array_wildcard_fields.get(field) {
Some(f) => f,
None => {
let msg = format!(
"Wildcard field '{field}[*]' present in intersection but missing in old_table '{}'",
table_name
);
tracing::error!(table = %table_name, field = %field, "{}", msg);
return Err(EvenframeError::comparison(msg));
}
};
let new_wild = match new_table.array_wildcard_fields.get(field) {
Some(f) => f,
None => {
let msg = format!(
"Wildcard field '{field}[*]' present in intersection but missing in new_table '{}'",
table_name
);
tracing::error!(table = %table_name, field = %field, "{}", msg);
return Err(EvenframeError::comparison(msg));
}
};
if let Some(field_change) =
Self::compare_fields(&format!("{}[*]", field), old_wild, new_wild)
{
table_changes.modified_fields.push(field_change);
}
}
let old_events: HashSet<String> = old_table.events.iter().cloned().collect();
let new_events: HashSet<String> = new_table.events.iter().cloned().collect();
for event in new_events.difference(&old_events) {
table_changes.new_events.push(event.clone());
}
for event in old_events.difference(&new_events) {
table_changes.removed_events.push(event.clone());
}
if table_changes.new_fields.is_empty()
&& table_changes.removed_fields.is_empty()
&& table_changes.modified_fields.is_empty()
&& !table_changes.permission_changed
&& !table_changes.schema_type_changed
&& table_changes.new_events.is_empty()
&& table_changes.removed_events.is_empty()
{
Ok(None)
} else {
Ok(Some(table_changes))
}
}
fn compare_fields(
field_name: &str,
old_field: &FieldDefinition,
new_field: &FieldDefinition,
) -> Option<FieldChange> {
let mut changed = false;
let mut basic_change = FieldChange {
field_name: field_name.to_string(),
old_type: old_field.field_type.to_string(),
new_type: new_field.field_type.to_string(),
change_type: ChangeType::Modified,
required_changed: false,
default_changed: false,
};
if old_field.required != new_field.required {
basic_change.required_changed = true;
changed = true;
}
if old_field.default_value != new_field.default_value {
basic_change.default_changed = true;
changed = true;
}
if old_field.field_type != new_field.field_type {
changed = true;
}
if changed { Some(basic_change) } else { None }
}
pub fn compare_object_types(
prefix: &str,
old_type: &ObjectType,
new_type: &ObjectType,
) -> Vec<FieldChange> {
let mut changes = Vec::new();
match (old_type, new_type) {
(ObjectType::Object(old_fields), ObjectType::Object(new_fields)) => {
let old_keys: HashSet<String> = old_fields.keys().cloned().collect();
let new_keys: HashSet<String> = new_fields.keys().cloned().collect();
for key in new_keys.difference(&old_keys) {
let field_path = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
changes.push(FieldChange {
field_name: field_path,
old_type: String::new(),
new_type: new_fields[key].to_string(),
change_type: ChangeType::Added,
required_changed: false,
default_changed: false,
});
}
for key in old_keys.difference(&new_keys) {
let field_path = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
changes.push(FieldChange {
field_name: field_path,
old_type: old_fields[key].to_string(),
new_type: String::new(),
change_type: ChangeType::Removed,
required_changed: false,
default_changed: false,
});
}
for key in old_keys.intersection(&new_keys) {
let field_path = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
let old_field_type = &old_fields[key];
let new_field_type = &new_fields[key];
if old_field_type != new_field_type {
let nested_changes =
Self::compare_object_types(&field_path, old_field_type, new_field_type);
if nested_changes.is_empty() {
changes.push(FieldChange {
field_name: field_path,
old_type: old_field_type.to_string(),
new_type: new_field_type.to_string(),
change_type: ChangeType::Modified,
required_changed: false,
default_changed: false,
});
} else {
changes.extend(nested_changes);
}
}
}
}
(ObjectType::Nullable(old_inner), ObjectType::Nullable(new_inner)) => {
changes.extend(Self::compare_object_types(prefix, old_inner, new_inner));
}
_ => {
if old_type != new_type {
changes.push(FieldChange {
field_name: prefix.to_string(),
old_type: old_type.to_string(),
new_type: new_type.to_string(),
change_type: ChangeType::Modified,
required_changed: false,
default_changed: false,
});
}
}
}
changes
}
fn compare_accesses(
old_access: &AccessDefinition,
new_access: &AccessDefinition,
) -> Option<AccessChange> {
let mut changes = Vec::new();
if old_access.access_type != new_access.access_type {
changes.push(AccessChangeType::OtherChange(format!(
"Access type changed from {:?} to {:?}",
old_access.access_type, new_access.access_type
)));
}
if old_access.database_level != new_access.database_level {
let old_level = if old_access.database_level {
"DATABASE"
} else {
"NAMESPACE"
};
let new_level = if new_access.database_level {
"DATABASE"
} else {
"NAMESPACE"
};
changes.push(AccessChangeType::OtherChange(format!(
"Access level changed from {} to {}",
old_level, new_level
)));
}
if old_access.signup_query != new_access.signup_query {
changes.push(AccessChangeType::SignupChanged);
}
if old_access.signin_query != new_access.signin_query {
changes.push(AccessChangeType::SigninChanged);
}
if old_access.jwt_algorithm != new_access.jwt_algorithm {
changes.push(AccessChangeType::OtherChange(format!(
"JWT algorithm changed from {:?} to {:?}",
old_access.jwt_algorithm, new_access.jwt_algorithm
)));
}
if old_access.jwt_key != new_access.jwt_key {
changes.push(AccessChangeType::JwtKeyChanged);
}
if old_access.jwt_url != new_access.jwt_url {
changes.push(AccessChangeType::JwtUrlChanged);
}
if old_access.issuer_key != new_access.issuer_key {
changes.push(AccessChangeType::IssuerKeyChanged);
}
if old_access.authenticate != new_access.authenticate {
changes.push(AccessChangeType::AuthenticateClauseChanged);
}
if old_access.duration_for_token != new_access.duration_for_token {
changes.push(AccessChangeType::DurationChanged);
}
if old_access.duration_for_session != new_access.duration_for_session {
changes.push(AccessChangeType::DurationChanged);
}
if old_access.bearer_for != new_access.bearer_for {
changes.push(AccessChangeType::OtherChange(format!(
"Bearer FOR changed from {:?} to {:?}",
old_access.bearer_for, new_access.bearer_for
)));
}
if changes.is_empty() {
None
} else {
Some(AccessChange {
access_name: old_access.name.clone(),
changes,
})
}
}
}
pub fn collect_referenced_objects(
field_type: &FieldType,
objects_to_process: &mut Vec<String>,
enums: &HashMap<String, TaggedUnion>,
) {
match field_type {
FieldType::Other(type_name) => {
if let Some(enum_def) = enums.get(type_name) {
for variant in &enum_def.variants {
if let Some(variant_data) = &variant.data {
match variant_data {
VariantData::InlineStruct(enum_struct) => {
objects_to_process.push(enum_struct.struct_name.clone())
}
VariantData::DataStructureRef(referenced_field_type) => {
if let FieldType::Other(data) = referenced_field_type {
objects_to_process.push(data.clone());
}
}
}
}
}
} else {
objects_to_process.push(type_name.clone());
}
}
FieldType::Option(inner) | FieldType::Vec(inner) | FieldType::RecordLink(inner) => {
collect_referenced_objects(inner, objects_to_process, enums);
}
FieldType::Tuple(types) => {
for t in types {
collect_referenced_objects(t, objects_to_process, enums);
}
}
FieldType::Struct(fields) => {
for (_, field_type) in fields {
collect_referenced_objects(field_type, objects_to_process, enums);
}
}
FieldType::HashMap(key_type, value_type) | FieldType::BTreeMap(key_type, value_type) => {
collect_referenced_objects(key_type, objects_to_process, enums);
collect_referenced_objects(value_type, objects_to_process, enums);
}
_ => {}
}
}
#[cfg(feature = "surrealdb")]
pub struct Merger<'a> {
pub client: &'a Surreal<Client>,
pub default_mock_gen_config: SchemasyncMockGenConfig,
pub performance: PerformanceConfig,
}
#[cfg(feature = "surrealdb")]
impl<'a> Merger<'a> {
pub async fn new(
client: &'a Surreal<Client>,
default_mock_gen_config: SchemasyncMockGenConfig,
performance: PerformanceConfig,
) -> Result<Self> {
Ok(Self {
client,
default_mock_gen_config,
performance,
})
}
pub async fn import_schema_from_db(&self) -> Result<SchemaDefinition> {
tracing::debug!("Importing schema from production database");
let importer = SchemaImporter::new(self.client);
let schema = importer.import_schema_only().await?;
tracing::debug!(
tables = schema.tables.len(),
edges = schema.edges.len(),
accesses = schema.accesses.len(),
"Schema imported"
);
Ok(schema)
}
pub fn generate_schema_from_structs(
&self,
tables: &HashMap<String, TableConfig>,
) -> Result<SchemaDefinition> {
tracing::debug!(
table_count = tables.len(),
"Generating schema from Rust structs"
);
let schema = SchemaDefinition::from_table_configs(tables)?;
tracing::debug!(
tables = schema.tables.len(),
edges = schema.edges.len(),
"Schema generated from structs"
);
Ok(schema)
}
pub fn compare_schemas(
&self,
old: &SchemaDefinition,
new: &SchemaDefinition,
) -> Result<SchemaChanges> {
tracing::debug!("Comparing schemas using legacy method");
Comparator::compare(old, new)
}
pub async fn export_mock_data(&self, _file_path: &str) -> Result<()> {
todo!("Implement export_mock_data")
}
pub async fn generate_preserved_data(
&self,
table_name: &str,
table_config: &TableConfig,
mock_config: MockGenerationConfig,
existing_records: Vec<serde_json::Value>,
target_count: usize,
schema_changes: Option<&SchemaChanges>,
) -> Vec<serde_json::Value> {
use serde_json::Value;
let existing_count = existing_records.len();
let mut result = Vec::new();
match mock_config.preservation_mode {
PreservationMode::None => {
result = self.generate_new_records(table_name, table_config, target_count);
}
PreservationMode::Smart => {
if existing_count > 0 {
let mut fields_to_regenerate = mock_config.regenerate_fields.clone();
if let Some(changes) = schema_changes {
let schema_fields_needing_generation =
changes.get_fields_needing_generation(table_name);
if schema_fields_needing_generation.contains(&"*".to_string()) {
result =
self.generate_new_records(table_name, table_config, target_count);
return result;
}
for field in schema_fields_needing_generation {
if !fields_to_regenerate.contains(&field) {
fields_to_regenerate.push(field);
}
}
}
for mut record in existing_records {
if let Value::Object(ref mut map) = record {
for field in &table_config.struct_config.fields {
if !map.contains_key(&field.field_name) {
let new_value = Self::generate_field_value(field, table_config);
map.insert(field.field_name.clone(), new_value);
}
}
for field_name in &fields_to_regenerate {
if let Some(field) = table_config
.struct_config
.fields
.iter()
.find(|f| &f.field_name == field_name)
{
let new_value = Self::generate_field_value(field, table_config);
map.insert(field_name.clone(), new_value);
}
}
}
result.push(record);
}
if target_count > existing_count {
let additional = self.generate_new_records(
table_name,
table_config,
target_count - existing_count,
);
result.extend(additional);
}
} else {
result = self.generate_new_records(table_name, table_config, target_count);
}
}
PreservationMode::Full => {
if existing_count > 0 {
if target_count < existing_count {
eprintln!(
"\n WARNING: Full preservation mode with data reduction detected!"
);
eprintln!(
" Table '{}' has {} existing records but target count is set to {}",
table_name, existing_count, target_count
);
eprintln!(
" This will DELETE {} records!",
existing_count - target_count
);
eprintln!("\n Options:");
eprintln!(
" 1. Change the target count (n) to {} or higher to preserve all records",
existing_count
);
eprintln!(" 2. Use Smart preservation mode instead of Full");
eprintln!(
" 3. Set preservation_mode to None if you want to regenerate all data"
);
eprintln!(
"\n In a production environment, this would require user confirmation."
);
eprintln!(
" For now, proceeding with target count of {} records.\n",
target_count
);
for mut record in existing_records {
if let Value::Object(ref mut map) = record {
for field in &table_config.struct_config.fields {
if !map.contains_key(&field.field_name) {
let new_value =
Self::generate_field_value(field, table_config);
map.insert(field.field_name.clone(), new_value);
}
}
}
result.push(record);
}
} else {
for mut record in existing_records {
if let Value::Object(ref mut map) = record {
for field in &table_config.struct_config.fields {
if !map.contains_key(&field.field_name) {
let new_value =
Self::generate_field_value(field, table_config);
map.insert(field.field_name.clone(), new_value);
}
}
}
result.push(record);
}
if target_count > existing_count {
let additional = self.generate_new_records(
table_name,
table_config,
target_count - existing_count,
);
result.extend(additional);
}
}
} else {
result = self.generate_new_records(table_name, table_config, target_count);
}
}
}
result
}
fn generate_new_records(
&self,
_table_name: &str,
table_config: &TableConfig,
count: usize,
) -> Vec<serde_json::Value> {
use serde_json::Value;
let mut records = Vec::new();
for _ in 0..count {
let mut record = serde_json::Map::new();
for field in &table_config.struct_config.fields {
let value = Self::generate_field_value(field, table_config);
record.insert(field.field_name.clone(), value);
}
records.push(Value::Object(record));
}
records
}
fn generate_field_value(
field: &crate::types::StructField,
_table_config: &TableConfig,
) -> serde_json::Value {
use crate::types::FieldType;
use serde_json::json;
if let Some(format) = &field.format {
let value = format.generate_formatted_value();
match format {
crate::schemasync::mockmake::format::Format::Percentage
| crate::schemasync::mockmake::format::Format::Latitude
| crate::schemasync::mockmake::format::Format::Longitude
| crate::schemasync::mockmake::format::Format::CurrencyAmount => {
if let Ok(num) = value.parse::<f64>() {
return json!(num);
}
}
_ => {}
}
return json!(value);
}
match &field.field_type {
FieldType::String => json!(crate::schemasync::Mockmaker::random_string(8)),
FieldType::Bool => json!(rand::random::<bool>()),
FieldType::U8
| FieldType::U16
| FieldType::U32
| FieldType::U64
| FieldType::U128
| FieldType::Usize => json!(rand::random::<u32>() % 100),
FieldType::I8
| FieldType::I16
| FieldType::I32
| FieldType::I64
| FieldType::I128
| FieldType::Isize => json!(rand::random::<i32>() % 100),
FieldType::F32 | FieldType::F64 => json!(rand::random::<f64>() * 100.0),
FieldType::Option(inner) => {
if rand::random::<bool>() {
let inner_field = crate::types::StructField {
field_name: field.field_name.clone(),
field_type: *inner.clone(),
format: field.format.clone(),
..Default::default()
};
Self::generate_field_value(&inner_field, _table_config)
} else {
json!(null)
}
}
FieldType::Vec(_) => json!([]),
FieldType::Other(type_name) => {
if type_name.contains("DateTime") {
json!(chrono::Utc::now().to_rfc3339())
} else {
json!(format!("{}:1", type_name.to_lowercase()))
}
}
_ => json!(null),
}
}
}