#![allow(dead_code)]
#![allow(unused_variables)]
use crate::storage::{StorageEngine, BranchManager};
use crate::Result;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiffTarget {
Branch(String),
BranchAtLsn {
branch: String,
lsn: u64,
},
BranchAtScn {
branch: String,
scn: u64,
},
Lsn(u64),
Scn(u64),
Current,
}
impl DiffTarget {
pub fn parse(input: &str) -> Result<Self> {
let input = input.trim();
if input.eq_ignore_ascii_case("@current") || input.eq_ignore_ascii_case("HEAD") {
return Ok(DiffTarget::Current);
}
if let Some(rest) = input.strip_prefix("@lsn:") {
let lsn = rest.parse::<u64>()
.map_err(|_| crate::Error::query_execution(format!("Invalid LSN: {}", rest)))?;
return Ok(DiffTarget::Lsn(lsn));
}
if let Some(rest) = input.strip_prefix("@scn:") {
let scn = rest.parse::<u64>()
.map_err(|_| crate::Error::query_execution(format!("Invalid SCN: {}", rest)))?;
return Ok(DiffTarget::Scn(scn));
}
if let Some(at_pos) = input.find('@') {
let branch = input[..at_pos].to_string();
let qualifier = &input[at_pos + 1..];
if let Some(lsn_str) = qualifier.strip_prefix("lsn:") {
let lsn = lsn_str.parse::<u64>()
.map_err(|_| crate::Error::query_execution(format!("Invalid LSN: {}", lsn_str)))?;
return Ok(DiffTarget::BranchAtLsn { branch, lsn });
}
if let Some(scn_str) = qualifier.strip_prefix("scn:") {
let scn = scn_str.parse::<u64>()
.map_err(|_| crate::Error::query_execution(format!("Invalid SCN: {}", scn_str)))?;
return Ok(DiffTarget::BranchAtScn { branch, scn });
}
return Err(crate::Error::query_execution(
format!("Invalid diff target qualifier: {}. Use @lsn:N or @scn:N", qualifier)
));
}
Ok(DiffTarget::Branch(input.to_string()))
}
pub fn branch_name(&self) -> Option<&str> {
match self {
DiffTarget::Branch(name) => Some(name),
DiffTarget::BranchAtLsn { branch, .. } => Some(branch),
DiffTarget::BranchAtScn { branch, .. } => Some(branch),
DiffTarget::Lsn(_) | DiffTarget::Scn(_) | DiffTarget::Current => None,
}
}
pub fn lsn(&self) -> Option<u64> {
match self {
DiffTarget::BranchAtLsn { lsn, .. } => Some(*lsn),
DiffTarget::Lsn(lsn) => Some(*lsn),
_ => None,
}
}
pub fn scn(&self) -> Option<u64> {
match self {
DiffTarget::BranchAtScn { scn, .. } => Some(*scn),
DiffTarget::Scn(scn) => Some(*scn),
_ => None,
}
}
pub fn display(&self) -> String {
match self {
DiffTarget::Branch(name) => name.clone(),
DiffTarget::BranchAtLsn { branch, lsn } => format!("{}@lsn:{}", branch, lsn),
DiffTarget::BranchAtScn { branch, scn } => format!("{}@scn:{}", branch, scn),
DiffTarget::Lsn(lsn) => format!("@lsn:{}", lsn),
DiffTarget::Scn(scn) => format!("@scn:{}", scn),
DiffTarget::Current => "HEAD".to_string(),
}
}
}
impl std::fmt::Display for DiffTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffSpec {
pub source: DiffTarget,
pub target: DiffTarget,
pub tables: Option<Vec<String>>,
}
impl DiffSpec {
pub fn new(source: DiffTarget, target: DiffTarget) -> Self {
Self {
source,
target,
tables: None,
}
}
pub fn branches(source: &str, target: &str) -> Self {
Self::new(
DiffTarget::Branch(source.to_string()),
DiffTarget::Branch(target.to_string()),
)
}
pub fn lsn_range(branch: &str, from_lsn: u64, to_lsn: u64) -> Self {
Self::new(
DiffTarget::BranchAtLsn { branch: branch.to_string(), lsn: from_lsn },
DiffTarget::BranchAtLsn { branch: branch.to_string(), lsn: to_lsn },
)
}
pub fn scn_range(branch: &str, from_scn: u64, to_scn: u64) -> Self {
Self::new(
DiffTarget::BranchAtScn { branch: branch.to_string(), scn: from_scn },
DiffTarget::BranchAtScn { branch: branch.to_string(), scn: to_scn },
)
}
pub fn with_tables(mut self, tables: Vec<String>) -> Self {
self.tables = Some(tables);
self
}
pub fn parse(input: &str) -> Result<Self> {
if let Some(sep_pos) = input.find("..") {
let source_str = &input[..sep_pos];
let target_str = &input[sep_pos + 2..];
let source = DiffTarget::parse(source_str)?;
let target = DiffTarget::parse(target_str)?;
return Ok(Self::new(source, target));
}
Err(crate::Error::query_execution(
format!("Invalid diff spec: {}. Use format 'source..target'", input)
))
}
pub fn is_same_branch_diff(&self) -> bool {
match (&self.source, &self.target) {
(DiffTarget::BranchAtLsn { branch: b1, .. }, DiffTarget::BranchAtLsn { branch: b2, .. }) => b1 == b2,
(DiffTarget::BranchAtScn { branch: b1, .. }, DiffTarget::BranchAtScn { branch: b2, .. }) => b1 == b2,
(DiffTarget::Lsn(_), DiffTarget::Lsn(_)) => true,
(DiffTarget::Scn(_), DiffTarget::Scn(_)) => true,
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeTravelDiff {
pub source: DiffTarget,
pub target: DiffTarget,
pub schema_diff: SchemaDiff,
pub data_changes: Option<Vec<TableDataDiff>>,
pub row_counts: Option<Vec<RowCountDiff>>,
pub stats: DiffStats,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DiffStats {
pub rows_added: u64,
pub rows_removed: u64,
pub rows_modified: u64,
pub tables_changed: usize,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffLevel {
SchemaOnly,
Sampled { sample_size: usize },
Full,
}
impl Default for DiffLevel {
fn default() -> Self {
DiffLevel::SchemaOnly
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffFormat {
Unified,
Sql,
Json,
}
impl Default for DiffFormat {
fn default() -> Self {
DiffFormat::Unified
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSchemaDiff {
pub table_name: String,
pub change_type: SchemaChangeType,
pub column_changes: Vec<ColumnChange>,
pub index_changes: Vec<IndexChange>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SchemaChangeType {
Added,
Removed,
Modified,
Unchanged,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnChange {
pub column_name: String,
pub change_type: SchemaChangeType,
pub old_type: Option<String>,
pub new_type: Option<String>,
pub old_nullable: Option<bool>,
pub new_nullable: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexChange {
pub index_name: String,
pub change_type: SchemaChangeType,
pub old_columns: Option<Vec<String>>,
pub new_columns: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaDiff {
pub source_branch: String,
pub target_branch: String,
pub table_diffs: Vec<TableSchemaDiff>,
pub tables_added: usize,
pub tables_removed: usize,
pub tables_modified: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RowCountDiff {
pub table_name: String,
pub source_count: u64,
pub target_count: u64,
pub difference: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SampledDiff {
pub schema: SchemaDiff,
pub row_counts: Vec<RowCountDiff>,
pub samples: Vec<TableSample>,
pub sample_size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSample {
pub table_name: String,
pub added_rows: Vec<serde_json::Value>,
pub removed_rows: Vec<serde_json::Value>,
pub modified_rows: Vec<(serde_json::Value, serde_json::Value)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FullDiff {
pub schema: SchemaDiff,
pub data_changes: Vec<TableDataDiff>,
pub total_rows_added: u64,
pub total_rows_removed: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableDataDiff {
pub table_name: String,
pub added_rows: Vec<serde_json::Value>,
pub removed_rows: Vec<serde_json::Value>,
}
pub struct DiffEngine<'a> {
storage: Option<&'a StorageEngine>,
branch_manager: Option<Arc<BranchManager>>,
}
impl Default for DiffEngine<'_> {
fn default() -> Self {
Self::new()
}
}
impl<'a> DiffEngine<'a> {
pub fn new() -> Self {
Self {
storage: None,
branch_manager: None,
}
}
pub fn with_storage(storage: &'a StorageEngine) -> Self {
let branch_manager = storage.branch_manager();
Self {
storage: Some(storage),
branch_manager,
}
}
pub fn diff_schema(&self, source: &str, target: &str) -> Result<SchemaDiff> {
let storage = match self.storage {
Some(s) => s,
None => {
return Ok(SchemaDiff {
source_branch: source.to_string(),
target_branch: target.to_string(),
table_diffs: Vec::new(),
tables_added: 0,
tables_removed: 0,
tables_modified: 0,
});
}
};
let catalog = storage.catalog();
let all_tables = catalog.list_tables().unwrap_or_default();
let mut table_diffs = Vec::new();
let tables_added = 0;
let tables_removed = 0;
let tables_modified = 0;
if let Some(ref bm) = self.branch_manager {
let _source_branch = bm.get_branch_by_name(source).ok();
let _target_branch = bm.get_branch_by_name(target).ok();
}
for table_name in &all_tables {
if let Ok(schema) = catalog.get_table_schema(table_name) {
table_diffs.push(TableSchemaDiff {
table_name: table_name.clone(),
change_type: SchemaChangeType::Unchanged,
column_changes: Vec::new(),
index_changes: Vec::new(),
});
}
}
Ok(SchemaDiff {
source_branch: source.to_string(),
target_branch: target.to_string(),
table_diffs,
tables_added,
tables_removed,
tables_modified,
})
}
pub fn diff_sampled(&self, source: &str, target: &str, sample_size: usize) -> Result<SampledDiff> {
let schema = self.diff_schema(source, target)?;
let mut row_counts = Vec::new();
if let Some(storage) = self.storage {
let catalog = storage.catalog();
for table_diff in &schema.table_diffs {
if table_diff.change_type != SchemaChangeType::Removed {
let count = catalog.get_table_statistics(&table_diff.table_name)
.ok()
.flatten()
.map(|s| s.row_count)
.unwrap_or(0);
row_counts.push(RowCountDiff {
table_name: table_diff.table_name.clone(),
source_count: count, target_count: count, difference: 0,
});
}
}
}
let samples = Vec::new();
Ok(SampledDiff {
schema,
row_counts,
samples,
sample_size,
})
}
pub fn diff_full(&self, source: &str, target: &str, tables: Option<&[String]>) -> Result<FullDiff> {
let schema = self.diff_schema(source, target)?;
let mut data_changes = Vec::new();
let total_rows_added = 0u64;
let total_rows_removed = 0u64;
let tables_to_diff: Vec<String> = if let Some(specific_tables) = tables {
specific_tables.to_vec()
} else {
schema.table_diffs.iter()
.filter(|d| d.change_type != SchemaChangeType::Removed)
.map(|d| d.table_name.clone())
.collect()
};
for table_name in tables_to_diff {
data_changes.push(TableDataDiff {
table_name,
added_rows: Vec::new(),
removed_rows: Vec::new(),
});
}
Ok(FullDiff {
schema,
data_changes,
total_rows_added,
total_rows_removed,
})
}
pub fn diff_with_spec(&self, spec: &DiffSpec, level: DiffLevel) -> Result<TimeTravelDiff> {
let start_time = std::time::Instant::now();
let schema_diff = self.diff_schema_for_targets(&spec.source, &spec.target)?;
let (data_changes, row_counts) = match level {
DiffLevel::SchemaOnly => (None, None),
DiffLevel::Sampled { sample_size } => {
let counts = self.get_row_counts_for_targets(&spec.source, &spec.target, &schema_diff)?;
(None, Some(counts))
}
DiffLevel::Full => {
let (changes, counts) = self.get_full_diff_for_targets(
&spec.source,
&spec.target,
spec.tables.as_deref(),
&schema_diff,
)?;
(Some(changes), Some(counts))
}
};
let mut stats = DiffStats::default();
stats.tables_changed = schema_diff.table_diffs.iter()
.filter(|t| t.change_type != SchemaChangeType::Unchanged)
.count();
if let Some(ref changes) = data_changes {
for change in changes {
stats.rows_added += change.added_rows.len() as u64;
stats.rows_removed += change.removed_rows.len() as u64;
}
}
stats.duration_ms = start_time.elapsed().as_millis() as u64;
Ok(TimeTravelDiff {
source: spec.source.clone(),
target: spec.target.clone(),
schema_diff,
data_changes,
row_counts,
stats,
})
}
pub fn diff_lsn(
&self,
branch: &str,
from_lsn: u64,
to_lsn: u64,
level: DiffLevel,
) -> Result<TimeTravelDiff> {
let spec = DiffSpec::lsn_range(branch, from_lsn, to_lsn);
self.diff_with_spec(&spec, level)
}
pub fn diff_scn(
&self,
branch: &str,
from_scn: u64,
to_scn: u64,
level: DiffLevel,
) -> Result<TimeTravelDiff> {
let spec = DiffSpec::scn_range(branch, from_scn, to_scn);
self.diff_with_spec(&spec, level)
}
pub fn diff_targets(
&self,
source: DiffTarget,
target: DiffTarget,
level: DiffLevel,
) -> Result<TimeTravelDiff> {
let spec = DiffSpec::new(source, target);
self.diff_with_spec(&spec, level)
}
fn diff_schema_for_targets(&self, source: &DiffTarget, target: &DiffTarget) -> Result<SchemaDiff> {
let source_name = source.branch_name().unwrap_or("main");
let target_name = target.branch_name().unwrap_or("main");
let mut schema = self.diff_schema(source_name, target_name)?;
schema.source_branch = source.display();
schema.target_branch = target.display();
Ok(schema)
}
fn get_row_counts_for_targets(
&self,
_source: &DiffTarget,
_target: &DiffTarget,
schema: &SchemaDiff,
) -> Result<Vec<RowCountDiff>> {
let mut counts = Vec::new();
if let Some(storage) = self.storage {
let catalog = storage.catalog();
for table_diff in &schema.table_diffs {
if table_diff.change_type != SchemaChangeType::Removed {
let count = catalog.get_table_statistics(&table_diff.table_name)
.ok()
.flatten()
.map(|s| s.row_count)
.unwrap_or(0);
counts.push(RowCountDiff {
table_name: table_diff.table_name.clone(),
source_count: count,
target_count: count,
difference: 0,
});
}
}
}
Ok(counts)
}
fn get_full_diff_for_targets(
&self,
source: &DiffTarget,
target: &DiffTarget,
tables: Option<&[String]>,
schema: &SchemaDiff,
) -> Result<(Vec<TableDataDiff>, Vec<RowCountDiff>)> {
let mut data_changes = Vec::new();
let mut row_counts = Vec::new();
let tables_to_diff: Vec<String> = if let Some(specific) = tables {
specific.to_vec()
} else {
schema.table_diffs.iter()
.filter(|d| d.change_type != SchemaChangeType::Removed)
.map(|d| d.table_name.clone())
.collect()
};
if let Some(storage) = self.storage {
let catalog = storage.catalog();
let source_ts = self.resolve_target_timestamp(source)?;
let target_ts = self.resolve_target_timestamp(target)?;
for table_name in tables_to_diff {
let count = catalog.get_table_statistics(&table_name)
.ok()
.flatten()
.map(|s| s.row_count)
.unwrap_or(0);
row_counts.push(RowCountDiff {
table_name: table_name.clone(),
source_count: count,
target_count: count,
difference: 0,
});
let table_diff = self.diff_table_data(&table_name, source_ts, target_ts)?;
data_changes.push(table_diff);
}
}
Ok((data_changes, row_counts))
}
fn resolve_target_timestamp(&self, target: &DiffTarget) -> Result<u64> {
match target {
DiffTarget::Current => {
Ok(u64::MAX)
}
DiffTarget::Branch(_) => {
Ok(u64::MAX)
}
DiffTarget::Lsn(lsn) | DiffTarget::BranchAtLsn { lsn, .. } => {
Ok(*lsn)
}
DiffTarget::Scn(scn) | DiffTarget::BranchAtScn { scn, .. } => {
if let Some(storage) = self.storage {
let sm = storage.snapshot_manager();
return sm.resolve_scn(*scn);
}
Ok(*scn)
}
}
}
fn diff_table_data(
&self,
table_name: &str,
source_ts: u64,
target_ts: u64,
) -> Result<TableDataDiff> {
let mut added_rows = Vec::new();
let mut removed_rows = Vec::new();
if let Some(storage) = self.storage {
let sm = storage.snapshot_manager();
let (start_ts, end_ts) = if source_ts < target_ts {
(source_ts, target_ts)
} else {
(target_ts, source_ts)
};
let versions = sm.scan_versions_between(table_name, start_ts, end_ts)?;
let mut row_versions: HashMap<u64, Vec<(u64, Vec<u8>)>> = HashMap::new();
for (row_id, ts, data) in versions {
row_versions.entry(row_id)
.or_default()
.push((ts, data));
}
for (row_id, versions) in row_versions {
let has_source = versions.iter().any(|(ts, _)| *ts <= source_ts);
let has_target = versions.iter().any(|(ts, _)| *ts <= target_ts);
let latest_data = versions.iter()
.max_by_key(|(ts, _)| *ts)
.map(|(_, data)| data);
if let Some(data) = latest_data {
let json_value = serde_json::from_slice::<serde_json::Value>(data)
.unwrap_or_else(|_| serde_json::json!({
"row_id": row_id,
"data": "<binary>"
}));
if has_target && !has_source {
added_rows.push(json_value);
} else if has_source && !has_target {
removed_rows.push(json_value);
}
}
}
}
Ok(TableDataDiff {
table_name: table_name.to_string(),
added_rows,
removed_rows,
})
}
pub fn format_time_travel(&self, diff: &TimeTravelDiff, format: DiffFormat) -> String {
match format {
DiffFormat::Unified => self.format_time_travel_unified(diff),
DiffFormat::Sql => self.format_time_travel_sql(diff),
DiffFormat::Json => serde_json::to_string_pretty(diff).unwrap_or_default(),
}
}
fn format_time_travel_unified(&self, diff: &TimeTravelDiff) -> String {
let mut output = String::new();
output.push_str(&format!(
"--- {}\n+++ {}\n",
diff.source.display(),
diff.target.display()
));
if !diff.schema_diff.table_diffs.is_empty() {
output.push_str("\n@@ Schema Changes @@\n");
output.push_str(&self.format_unified(&diff.schema_diff));
}
if let Some(ref changes) = diff.data_changes {
for table_change in changes {
if !table_change.added_rows.is_empty() || !table_change.removed_rows.is_empty() {
output.push_str(&format!("\n@@ Table: {} @@\n", table_change.table_name));
for row in &table_change.removed_rows {
output.push_str(&format!("- {}\n", row));
}
for row in &table_change.added_rows {
output.push_str(&format!("+ {}\n", row));
}
}
}
}
output.push_str(&format!(
"\n-- Stats: {} tables changed, +{} rows, -{} rows ({} ms)\n",
diff.stats.tables_changed,
diff.stats.rows_added,
diff.stats.rows_removed,
diff.stats.duration_ms
));
output
}
fn format_time_travel_sql(&self, diff: &TimeTravelDiff) -> String {
let mut output = String::new();
output.push_str(&format!(
"-- Diff: {} -> {}\n",
diff.source.display(),
diff.target.display()
));
output.push_str("-- Generated SQL to transform source to target state\n\n");
output.push_str(&self.format_sql(&diff.schema_diff));
if let Some(ref changes) = diff.data_changes {
for table_change in changes {
if !table_change.removed_rows.is_empty() {
output.push_str(&format!(
"\n-- Delete {} rows from {}\n",
table_change.removed_rows.len(),
table_change.table_name
));
output.push_str(&format!(
"-- DELETE FROM {} WHERE <pk> IN (...);\n",
table_change.table_name
));
}
if !table_change.added_rows.is_empty() {
output.push_str(&format!(
"\n-- Insert {} rows into {}\n",
table_change.added_rows.len(),
table_change.table_name
));
output.push_str(&format!(
"-- INSERT INTO {} VALUES (...);\n",
table_change.table_name
));
}
}
}
output
}
pub fn format(&self, diff: &SchemaDiff, format: DiffFormat) -> String {
match format {
DiffFormat::Unified => self.format_unified(diff),
DiffFormat::Sql => self.format_sql(diff),
DiffFormat::Json => serde_json::to_string_pretty(diff).unwrap_or_default(),
}
}
fn format_unified(&self, diff: &SchemaDiff) -> String {
let mut output = String::new();
output.push_str(&format!(
"--- {}\n+++ {}\n",
diff.source_branch, diff.target_branch
));
for table_diff in &diff.table_diffs {
match table_diff.change_type {
SchemaChangeType::Added => {
output.push_str(&format!("+ TABLE {}\n", table_diff.table_name));
}
SchemaChangeType::Removed => {
output.push_str(&format!("- TABLE {}\n", table_diff.table_name));
}
SchemaChangeType::Modified => {
output.push_str(&format!("~ TABLE {} (modified)\n", table_diff.table_name));
for col in &table_diff.column_changes {
match col.change_type {
SchemaChangeType::Added => {
output.push_str(&format!(" + COLUMN {}\n", col.column_name));
}
SchemaChangeType::Removed => {
output.push_str(&format!(" - COLUMN {}\n", col.column_name));
}
SchemaChangeType::Modified => {
output.push_str(&format!(
" ~ COLUMN {} ({:?} -> {:?})\n",
col.column_name, col.old_type, col.new_type
));
}
_ => {}
}
}
}
_ => {}
}
}
output
}
fn format_sql(&self, diff: &SchemaDiff) -> String {
let mut output = String::new();
output.push_str(&format!(
"-- Transform {} -> {}\n\n",
diff.source_branch, diff.target_branch
));
for table_diff in &diff.table_diffs {
match table_diff.change_type {
SchemaChangeType::Added => {
output.push_str(&format!("CREATE TABLE {} (\n", table_diff.table_name));
let columns: Vec<String> = table_diff.column_changes.iter()
.filter(|c| matches!(c.change_type, SchemaChangeType::Added))
.map(|c| {
let col_type = c.new_type.as_deref().unwrap_or("TEXT");
let nullable = if c.new_nullable == Some(false) { " NOT NULL" } else { "" };
format!(" {} {}{}", c.column_name, col_type, nullable)
})
.collect();
output.push_str(&columns.join(",\n"));
output.push_str("\n);\n");
}
SchemaChangeType::Removed => {
output.push_str(&format!("DROP TABLE IF EXISTS {};\n", table_diff.table_name));
}
SchemaChangeType::Modified => {
for col in &table_diff.column_changes {
match col.change_type {
SchemaChangeType::Added => {
output.push_str(&format!(
"ALTER TABLE {} ADD COLUMN {} {};\n",
table_diff.table_name,
col.column_name,
col.new_type.as_deref().unwrap_or("TEXT")
));
}
SchemaChangeType::Removed => {
output.push_str(&format!(
"ALTER TABLE {} DROP COLUMN {};\n",
table_diff.table_name, col.column_name
));
}
_ => {}
}
}
}
_ => {}
}
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diff_level_default() {
assert_eq!(DiffLevel::default(), DiffLevel::SchemaOnly);
}
#[test]
fn test_diff_format_default() {
assert_eq!(DiffFormat::default(), DiffFormat::Unified);
}
#[test]
fn test_diff_engine_creation() {
let engine = DiffEngine::new();
let diff = engine.diff_schema("main", "feature").unwrap();
assert_eq!(diff.source_branch, "main");
assert_eq!(diff.target_branch, "feature");
}
#[test]
fn test_diff_target_parse_branch() {
let target = DiffTarget::parse("main").unwrap();
assert_eq!(target, DiffTarget::Branch("main".to_string()));
}
#[test]
fn test_diff_target_parse_branch_at_lsn() {
let target = DiffTarget::parse("feature@lsn:100").unwrap();
assert_eq!(target, DiffTarget::BranchAtLsn {
branch: "feature".to_string(),
lsn: 100,
});
}
#[test]
fn test_diff_target_parse_branch_at_scn() {
let target = DiffTarget::parse("main@scn:50").unwrap();
assert_eq!(target, DiffTarget::BranchAtScn {
branch: "main".to_string(),
scn: 50,
});
}
#[test]
fn test_diff_target_parse_lsn_only() {
let target = DiffTarget::parse("@lsn:200").unwrap();
assert_eq!(target, DiffTarget::Lsn(200));
}
#[test]
fn test_diff_target_parse_scn_only() {
let target = DiffTarget::parse("@scn:300").unwrap();
assert_eq!(target, DiffTarget::Scn(300));
}
#[test]
fn test_diff_target_parse_current() {
let target = DiffTarget::parse("@current").unwrap();
assert_eq!(target, DiffTarget::Current);
let target2 = DiffTarget::parse("HEAD").unwrap();
assert_eq!(target2, DiffTarget::Current);
}
#[test]
fn test_diff_target_display() {
assert_eq!(DiffTarget::Branch("main".to_string()).display(), "main");
assert_eq!(
DiffTarget::BranchAtLsn { branch: "dev".to_string(), lsn: 100 }.display(),
"dev@lsn:100"
);
assert_eq!(
DiffTarget::BranchAtScn { branch: "prod".to_string(), scn: 50 }.display(),
"prod@scn:50"
);
assert_eq!(DiffTarget::Lsn(200).display(), "@lsn:200");
assert_eq!(DiffTarget::Scn(300).display(), "@scn:300");
assert_eq!(DiffTarget::Current.display(), "HEAD");
}
#[test]
fn test_diff_spec_parse() {
let spec = DiffSpec::parse("main..feature").unwrap();
assert_eq!(spec.source, DiffTarget::Branch("main".to_string()));
assert_eq!(spec.target, DiffTarget::Branch("feature".to_string()));
}
#[test]
fn test_diff_spec_parse_with_lsn() {
let spec = DiffSpec::parse("main@lsn:10..main@lsn:20").unwrap();
assert!(spec.is_same_branch_diff());
assert_eq!(spec.source, DiffTarget::BranchAtLsn {
branch: "main".to_string(),
lsn: 10,
});
assert_eq!(spec.target, DiffTarget::BranchAtLsn {
branch: "main".to_string(),
lsn: 20,
});
}
#[test]
fn test_diff_spec_parse_lsn_only() {
let spec = DiffSpec::parse("@lsn:100..@lsn:200").unwrap();
assert!(spec.is_same_branch_diff());
}
#[test]
fn test_diff_spec_lsn_range() {
let spec = DiffSpec::lsn_range("main", 100, 200);
assert!(spec.is_same_branch_diff());
assert_eq!(spec.source.lsn(), Some(100));
assert_eq!(spec.target.lsn(), Some(200));
}
#[test]
fn test_diff_spec_scn_range() {
let spec = DiffSpec::scn_range("main", 10, 20);
assert!(spec.is_same_branch_diff());
assert_eq!(spec.source.scn(), Some(10));
assert_eq!(spec.target.scn(), Some(20));
}
#[test]
fn test_diff_spec_branches() {
let spec = DiffSpec::branches("main", "feature");
assert!(!spec.is_same_branch_diff());
}
#[test]
fn test_time_travel_diff_lsn() {
let engine = DiffEngine::new();
let diff = engine.diff_lsn("main", 100, 200, DiffLevel::SchemaOnly).unwrap();
assert_eq!(diff.source.display(), "main@lsn:100");
assert_eq!(diff.target.display(), "main@lsn:200");
}
#[test]
fn test_time_travel_diff_scn() {
let engine = DiffEngine::new();
let diff = engine.diff_scn("main", 10, 20, DiffLevel::SchemaOnly).unwrap();
assert_eq!(diff.source.display(), "main@scn:10");
assert_eq!(diff.target.display(), "main@scn:20");
}
#[test]
fn test_time_travel_diff_with_spec() {
let engine = DiffEngine::new();
let spec = DiffSpec::parse("main@lsn:50..feature@scn:100").unwrap();
let diff = engine.diff_with_spec(&spec, DiffLevel::SchemaOnly).unwrap();
assert_eq!(diff.source.display(), "main@lsn:50");
assert_eq!(diff.target.display(), "feature@scn:100");
}
#[test]
fn test_time_travel_diff_format() {
let engine = DiffEngine::new();
let diff = engine.diff_lsn("main", 100, 200, DiffLevel::SchemaOnly).unwrap();
let unified = engine.format_time_travel(&diff, DiffFormat::Unified);
assert!(unified.contains("--- main@lsn:100"));
assert!(unified.contains("+++ main@lsn:200"));
let json = engine.format_time_travel(&diff, DiffFormat::Json);
assert!(json.contains("main@lsn:100"));
}
}