From b1125a0ab698bfd758ee4f286b73718aaa22829b Mon Sep 17 00:00:00 2001 From: iamvirul Date: Fri, 25 Sep 2026 08:43:51 +0530 Subject: [PATCH 1/7] feat(rowdiff): inspect column-level differences for changed rows cross-engine --- cmd/deepdiffdb/main.go | 186 +++++++++++++++++- internal/cli/display.go | 45 +++++ internal/content/resolve/fetch.go | 149 ++++++++------ internal/content/resolve/rowdiff.go | 289 ++++++++++++++++++++++++++++ tests/resolve/fetch_test.go | 111 +++++++++++ tests/resolve/rowdiff_test.go | 174 +++++++++++++++++ 6 files changed, 895 insertions(+), 59 deletions(-) create mode 100644 internal/content/resolve/rowdiff.go create mode 100644 tests/resolve/rowdiff_test.go diff --git a/cmd/deepdiffdb/main.go b/cmd/deepdiffdb/main.go index a23b864..cb5d48a 100644 --- a/cmd/deepdiffdb/main.go +++ b/cmd/deepdiffdb/main.go @@ -201,6 +201,8 @@ func run(args []string) error { return runSchemaMigrate(args[1:]) case "diff": return runFullDiff(args[1:]) + case "row-diff": + return runRowDiff(args[1:]) case "gen-pack": return runGenPack(args[1:]) case "apply": @@ -325,6 +327,7 @@ func runFullDiff(args []string) error { fs := flag.NewFlagSet("diff", flag.ContinueOnError) configPath := fs.String("config", "deepdiffdb.config.yaml", "Path to configuration file") generateHTML := fs.Bool("html", false, "Generate interactive HTML report") + showRowDiff := fs.Bool("row-diff", false, "Display and generate column-level diffs for changed rows") batchSizeFlag := fs.Int("batch-size", 0, "Rows per keyset-paginated query (0 = use config default)") parallelFlag := fs.Int("parallel", 0, "Max tables hashed concurrently (0 = use config default)") verbose := fs.Bool("verbose", false, "Enable verbose logging") @@ -468,6 +471,30 @@ func runFullDiff(args []string) error { if conflicts.HasConflicts() { fmt.Printf("Warning: %d conflicts detected. Review %s\n", len(conflicts.Conflicts), filepath.Join(cfg.Output.Dir, "conflicts.json")) } + + if *showRowDiff { + log.Info("fetching column-level row differences") + rowReport, err := resolve.FetchRowDiffReport( + ctx, prodDB, devDB, cfg.Prod.Driver, cfg.Dev.Driver, + prodSchema, devSchema, dataDiff, conflicts, resolve.RowDiffOptions{ + StatusFilter: "updated", + }, + ) + if err != nil { + log.Warn("failed to fetch row diffs", "error", err) + } else if rowReport.HasChanges() { + if err := resolve.WriteRowDiffReport(rowReport, cfg.Output.Dir); err != nil { + log.Warn("failed to write row diff report", "error", err) + } + display := cli.NewDisplay() + for _, trd := range rowReport.Tables { + for _, rd := range trd.Rows { + display.PrintRowDiff(rd.Table, rd.Key, string(rd.Status), rd.Columns) + } + } + fmt.Printf("Row-level column diffs written to %s and %s\n", filepath.Join(cfg.Output.Dir, "row_diff.json"), filepath.Join(cfg.Output.Dir, "row_diff.txt")) + } + } } else { fmt.Println("No data differences found.") } @@ -502,6 +529,158 @@ func runFullDiff(args []string) error { return schemaDriftErr } +// runRowDiff executes the "row-diff" command to inspect column-level differences for changed rows. +func runRowDiff(args []string) error { + fs := flag.NewFlagSet("row-diff", flag.ContinueOnError) + configPath := fs.String("config", "deepdiffdb.config.yaml", "Path to configuration file") + tableFlag := fs.String("table", "", "Filter by specific table name") + keyFlag := fs.String("key", "", "Filter by primary key value") + statusFlag := fs.String("status", "updated", "Row status to inspect: updated, added, removed, or all") + limitFlag := fs.Int("limit", 0, "Max rows to inspect per table (0 = unlimited)") + formatFlag := fs.String("format", "table", "Output format: table or json") + outputFlag := fs.String("output", "", "Output directory to save reports (default: config output dir)") + batchSizeFlag := fs.Int("batch-size", 0, "Rows per keyset-paginated query (0 = use config default)") + parallelFlag := fs.Int("parallel", 0, "Max tables hashed concurrently (0 = use config default)") + verbose := fs.Bool("verbose", false, "Enable verbose logging") + logFile := fs.String("log-file", "", "Write logs to file") + logLevel := fs.String("log-level", "info", "Log level: debug, info, warn, error") + logFormat := fs.String("log-format", "text", "Log format: text or json") + if err := fs.Parse(args); err != nil { + return err + } + + log, logCloser, err := initializeLogger(*verbose, *logFile, *logLevel, *logFormat) + if err != nil { + return err + } + if logCloser != nil { + defer logCloser.Close() + } + + log.Info("starting row-level data diff") + + cfg, err := config.Load(*configPath) + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + outDir := cfg.Output.Dir + if *outputFlag != "" { + outDir = *outputFlag + } + + ctx := logger.ToContext(context.Background(), log) + + prodDB, err := openDatabaseWithSpinner(ctx, cfg.Prod, "production") + if err != nil { + return fmt.Errorf("prod connection failed: %w", err) + } + defer prodDB.Close() + + devDB, err := openDatabaseWithSpinner(ctx, cfg.Dev, "development") + if err != nil { + return fmt.Errorf("dev connection failed: %w", err) + } + defer devDB.Close() + + log.Info("loading database schemas") + prodSchema, err := schema.LoadSchema(ctx, prodDB, cfg.Prod.Driver, cfg.Prod.Database, cfg.Ignore.Tables, schema.LoadSchemaOptions{IgnoreViews: cfg.Ignore.Views, IgnoreRoutines: cfg.Ignore.Routines, IgnoreTriggers: cfg.Ignore.Triggers, IgnoreSequences: cfg.Ignore.Sequences}) + if err != nil { + return fmt.Errorf("load prod schema: %w", err) + } + devSchema, err := schema.LoadSchema(ctx, devDB, cfg.Dev.Driver, cfg.Dev.Database, cfg.Ignore.Tables, schema.LoadSchemaOptions{IgnoreViews: cfg.Ignore.Views, IgnoreRoutines: cfg.Ignore.Routines, IgnoreTriggers: cfg.Ignore.Triggers, IgnoreSequences: cfg.Ignore.Sequences}) + if err != nil { + return fmt.Errorf("load dev schema: %w", err) + } + + var dataDiff content.DataDiff + var conflicts content.Conflicts + + // If table and key are specified, we can query the row directly without full table hashing + if *tableFlag != "" && *keyFlag != "" { + log.Info("direct row inspection for key", "table", *tableFlag, "key", *keyFlag) + } else { + ignoreColumn := content.IgnoreMatcher(cfg.Ignore.Columns) + batchSize := resolveBatchSize(*batchSizeFlag, cfg.Performance.HashBatchSize) + maxParallel := resolveParallel(*parallelFlag, cfg.Performance.MaxParallelTables) + + devByCanonical := make(map[string]schema.Table, len(devSchema.Tables)) + for name, devTable := range devSchema.Tables { + devByCanonical[schema.CanonicalIdent(name)] = devTable + } + sharedProdTables := make(map[string]schema.Table) + sharedDevTables := make(map[string]schema.Table) + for name, prodTable := range prodSchema.Tables { + if *tableFlag != "" && schema.CanonicalIdent(name) != schema.CanonicalIdent(*tableFlag) { + continue + } + if devTable, ok := devByCanonical[schema.CanonicalIdent(name)]; ok { + sharedProdTables[name] = prodTable + sharedDevTables[devTable.Name] = devTable + } + } + + if len(sharedProdTables) == 0 { + if *tableFlag != "" { + return fmt.Errorf("table %q not found in both databases", *tableFlag) + } + fmt.Println("No matching tables found to compare.") + return nil + } + + log.Info("hashing tables to detect row differences", "table_count", len(sharedProdTables)) + prodHashes, err := hashTablesParallel(ctx, sharedProdTables, prodDB, cfg.Prod.Driver, ignoreColumn, batchSize, maxParallel) + if err != nil { + return fmt.Errorf("hash prod tables: %w", err) + } + devHashes, err := hashTablesParallel(ctx, sharedDevTables, devDB, cfg.Dev.Driver, ignoreColumn, batchSize, maxParallel) + if err != nil { + return fmt.Errorf("hash dev tables: %w", err) + } + dataDiff, conflicts = content.BuildDataDiff(prodSchema, devSchema, prodHashes, devHashes) + } + + opts := resolve.RowDiffOptions{ + TableFilter: *tableFlag, + KeyFilter: *keyFlag, + StatusFilter: *statusFlag, + Limit: *limitFlag, + } + + report, err := resolve.FetchRowDiffReport(ctx, prodDB, devDB, cfg.Prod.Driver, cfg.Dev.Driver, prodSchema, devSchema, dataDiff, conflicts, opts) + if err != nil { + return fmt.Errorf("fetch row diff report: %w", err) + } + + if err := resolve.WriteRowDiffReport(report, outDir); err != nil { + log.Warn("failed to write row diff report", "error", err) + } + + if *formatFlag == "json" { + data, err := json.MarshalIndent(report, "", " ") + if err != nil { + return fmt.Errorf("marshal json: %w", err) + } + fmt.Println(string(data)) + return nil + } + + if !report.HasChanges() { + fmt.Println("No row-level differences found matching criteria.") + return nil + } + + display := cli.NewDisplay() + for _, trd := range report.Tables { + for _, rd := range trd.Rows { + display.PrintRowDiff(rd.Table, rd.Key, string(rd.Status), rd.Columns) + } + } + fmt.Printf("Total differing rows inspected: %d\n", report.TotalRows()) + fmt.Printf("Row diff report written to %s and %s\n", filepath.Join(outDir, "row_diff.json"), filepath.Join(outDir, "row_diff.txt")) + return nil +} + // runGenPack performs the "gen-pack" command: it loads configuration, compares prod and dev schemas, and generates a migration pack for any detected data differences while writing schema and data reports to the configured output directory. // It opens prod and dev database connections, ensures the output directory exists, fails if schema drift is detected (after writing schema reports), computes per-table content hashes with configured ignore rules, builds the data diff and any conflicts, generates a migration pack when changes exist, writes data reports including the pack path and conflicts, and prints a brief status summary. // It returns a non-nil error for failures during configuration loading, database connections, schema loading or reporting, hashing, pack generation, or report writing; it also returns an error when schema drift is detected so the pack is not produced. @@ -1090,8 +1269,8 @@ func runResolveConflicts(args []string) error { } // Fetch row data for comparison - prod, dev, err := resolve.FetchConflictRows( - ctx, prodDB, devDB, cfg.Prod.Driver, + prod, dev, err := resolve.FetchConflictRowsCrossEngine( + ctx, prodDB, devDB, cfg.Prod.Driver, cfg.Dev.Driver, prodSchema, devSchema, res.Conflict, ) if err != nil { @@ -1484,7 +1663,8 @@ Commands: check Validate configuration and show quick summary schema-diff Detect schema drift schema-migrate Generate schema migration script - diff Full diff: schema + data (supports --html for interactive report) + diff Full diff: schema + data (supports --html, --row-diff) + row-diff Inspect changed row data in columns (supports --table, --key, --status, --format) gen-pack Generate SQL migration pack (supports --html, --resume) apply Apply migration pack (supports --resume) resolve-conflicts Interactively resolve pending conflicts (supports --resume) diff --git a/internal/cli/display.go b/internal/cli/display.go index 4978649..6c179c3 100644 --- a/internal/cli/display.go +++ b/internal/cli/display.go @@ -107,6 +107,51 @@ func (d *Display) PrintConflictComparison(prod, dev *resolve.RowData, diffs []re fmt.Fprintln(d.writer) } +// PrintRowDiff displays a side-by-side comparison of a row's column differences. +func (d *Display) PrintRowDiff(table, key, status string, diffs []resolve.ColumnDiff) { + d.PrintDoubleLine() + fmt.Fprintf(d.writer, "Table: %s | Key: %s (%s)\n", table, key, status) + d.PrintDoubleLine() + + // Calculate column widths + colWidth := 20 + valWidth := 24 + + // Print header + fmt.Fprintf(d.writer, " %-*s | %-*s | %-*s\n", + colWidth, "Column", + valWidth, "Production", + valWidth, "Development") + fmt.Fprintf(d.writer, " %s+%s+%s\n", + strings.Repeat("-", colWidth+1), + strings.Repeat("-", valWidth+2), + strings.Repeat("-", valWidth+2)) + + // Print each column + for _, diff := range diffs { + prodStr := formatValueForDisplay(diff.ProdVal, valWidth) + devStr := formatValueForDisplay(diff.DevVal, valWidth) + + marker := "" + if diff.Differs { + marker = " *" + } + + fmt.Fprintf(d.writer, " %-*s | %-*s | %-*s%s\n", + colWidth, truncate(diff.Column, colWidth), + valWidth, prodStr, + valWidth, devStr, + marker) + } + + fmt.Fprintln(d.writer) + if hasDifferences(diffs) { + fmt.Fprintln(d.writer, " * indicates columns that differ") + } + d.PrintDoubleLine() + fmt.Fprintln(d.writer) +} + // PrintSummary displays the resolution summary. func (d *Display) PrintSummary(summary resolve.ResolutionSummary, outputPath string) { d.PrintDoubleLine() diff --git a/internal/content/resolve/fetch.go b/internal/content/resolve/fetch.go index 3885609..403638e 100644 --- a/internal/content/resolve/fetch.go +++ b/internal/content/resolve/fetch.go @@ -29,8 +29,26 @@ type ColumnDiff struct { Differs bool } -// FetchConflictRows fetches both production and development row data for a conflict. -// Returns nil for a row if it doesn't exist in that database. +// FindTableByCanonicalName searches a schema for a table matching tableName, +// trying exact match first, then canonical (case-folded) match. +func FindTableByCanonicalName(sch *schema.Schema, tableName string) (schema.Table, bool) { + if sch == nil || len(sch.Tables) == 0 { + return schema.Table{}, false + } + if tbl, ok := sch.Tables[tableName]; ok { + return tbl, true + } + canon := schema.CanonicalIdent(tableName) + for name, tbl := range sch.Tables { + if schema.CanonicalIdent(name) == canon { + return tbl, true + } + } + return schema.Table{}, false +} + +// FetchConflictRows fetches both production and development row data for a conflict +// using a single driver. Delegates to FetchConflictRowsCrossEngine. func FetchConflictRows( ctx context.Context, prodDB, devDB *sql.DB, @@ -38,36 +56,40 @@ func FetchConflictRows( prodSchema, devSchema *schema.Schema, conflict content.Conflict, ) (prod, dev *RowData, err error) { - // Get table schema from both databases - prodTable, prodExists := prodSchema.Tables[conflict.Table] - devTable, devExists := devSchema.Tables[conflict.Table] + return FetchConflictRowsCrossEngine(ctx, prodDB, devDB, driver, driver, prodSchema, devSchema, conflict) +} + +// FetchConflictRowsCrossEngine fetches both production and development row data for a conflict, +// supporting independent drivers (e.g. postgres vs oracle) and case-insensitive table matching. +// Returns nil for a row if it doesn't exist in that database. +func FetchConflictRowsCrossEngine( + ctx context.Context, + prodDB, devDB *sql.DB, + prodDriver, devDriver string, + prodSchema, devSchema *schema.Schema, + conflict content.Conflict, +) (prod, dev *RowData, err error) { + // Match table schema from both databases case-insensitively + prodTable, prodExists := FindTableByCanonicalName(prodSchema, conflict.Table) + devTable, devExists := FindTableByCanonicalName(devSchema, conflict.Table) if !prodExists && !devExists { return nil, nil, fmt.Errorf("table %s not found in either database", conflict.Table) } - // Use dev table schema as reference (it should have all columns) - var refTable schema.Table - if devExists { - refTable = devTable - } else { - refTable = prodTable - } - - // Get ordered column list - columns := getOrderedColumns(refTable) - - // Fetch from production + // Fetch from production using prod-specific table and columns if prodExists { - prod, err = fetchRowData(ctx, prodDB, driver, prodTable, columns, conflict.Key) + columns := getOrderedColumns(prodTable) + prod, err = FetchRowData(ctx, prodDB, prodDriver, prodTable, columns, conflict.Key) if err != nil && !isNoRowsError(err) { return nil, nil, fmt.Errorf("fetch prod row: %w", err) } } - // Fetch from development + // Fetch from development using dev-specific table and columns if devExists { - dev, err = fetchRowData(ctx, devDB, driver, devTable, columns, conflict.Key) + columns := getOrderedColumns(devTable) + dev, err = FetchRowData(ctx, devDB, devDriver, devTable, columns, conflict.Key) if err != nil && !isNoRowsError(err) { return nil, nil, fmt.Errorf("fetch dev row: %w", err) } @@ -76,8 +98,8 @@ func FetchConflictRows( return prod, dev, nil } -// fetchRowData fetches a single row from the database. -func fetchRowData( +// FetchRowData fetches a single row from the database using its primary key. +func FetchRowData( ctx context.Context, db *sql.DB, driver string, @@ -139,52 +161,65 @@ func fetchRowData( } // CompareRows compares two rows and returns the differences. +// It matches columns case-insensitively across engines (e.g. PostgreSQL "email" +// matches Oracle "EMAIL") while labeling with the prod-side column name. // Returns a list of column differences, with Differs=true for columns that don't match. func CompareRows(prod, dev *RowData) []ColumnDiff { if prod == nil && dev == nil { return nil } - // Collect all columns from both rows - colSet := make(map[string]bool) - if prod != nil { - for _, col := range prod.Columns { - colSet[col] = true - } - } + var diffs []ColumnDiff + matchedDev := make(map[string]bool) + + // Build dev canonical lookup map + devByCanonical := make(map[string]string) if dev != nil { for _, col := range dev.Columns { - colSet[col] = true + devByCanonical[schema.CanonicalIdent(col)] = col } } - // Sort columns for consistent output - columns := make([]string, 0, len(colSet)) - for col := range colSet { - columns = append(columns, col) - } - sort.Strings(columns) - - // Compare each column - diffs := make([]ColumnDiff, 0, len(columns)) - for _, col := range columns { - var prodVal, devVal any - if prod != nil { - prodVal = prod.Values[col] - } - if dev != nil { - devVal = dev.Values[col] + if prod != nil { + for _, pCol := range prod.Columns { + canon := schema.CanonicalIdent(pCol) + var devVal any + differs := true + if dCol, ok := devByCanonical[canon]; ok { + matchedDev[dCol] = true + if dev != nil { + devVal = dev.Values[dCol] + differs = !valuesEqual(prod.Values[pCol], devVal) + } + } + diffs = append(diffs, ColumnDiff{ + Column: pCol, + ProdVal: prod.Values[pCol], + DevVal: devVal, + Differs: differs, + }) } + } - differs := !valuesEqual(prodVal, devVal) - diffs = append(diffs, ColumnDiff{ - Column: col, - ProdVal: prodVal, - DevVal: devVal, - Differs: differs, - }) + // Dev columns that do not exist in prod + if dev != nil { + for _, dCol := range dev.Columns { + if !matchedDev[dCol] { + diffs = append(diffs, ColumnDiff{ + Column: dCol, + ProdVal: nil, + DevVal: dev.Values[dCol], + Differs: true, + }) + } + } } + // Sort columns alphabetically for deterministic output + sort.Slice(diffs, func(i, j int) bool { + return strings.ToLower(diffs[i].Column) < strings.ToLower(diffs[j].Column) + }) + return diffs } @@ -226,9 +261,11 @@ func splitKey(key string, expected int) ([]string, error) { func quoteIdent(driver, ident string) string { switch driver { case "mysql": - return "`" + ident + "`" - case "postgres", "postgresql": - return `"` + ident + `"` + return "`" + strings.ReplaceAll(ident, "`", "``") + "`" + case "postgres", "postgresql", "sqlite", "oracle": + return "\"" + strings.ReplaceAll(ident, "\"", "\"\"") + "\"" + case "mssql": + return "[" + strings.ReplaceAll(ident, "]", "]]") + "]" default: return ident } diff --git a/internal/content/resolve/rowdiff.go b/internal/content/resolve/rowdiff.go new file mode 100644 index 0000000..14b0699 --- /dev/null +++ b/internal/content/resolve/rowdiff.go @@ -0,0 +1,289 @@ +package resolve + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/iamvirul/deepdiff-db/internal/content" + "github.com/iamvirul/deepdiff-db/internal/schema" +) + +// RowDiffStatus represents the state of a row difference. +type RowDiffStatus string + +const ( + // StatusUpdated indicates the row exists in both prod and dev with differing values. + StatusUpdated RowDiffStatus = "updated" + // StatusAdded indicates the row exists only in development. + StatusAdded RowDiffStatus = "added" + // StatusRemoved indicates the row exists only in production. + StatusRemoved RowDiffStatus = "removed" +) + +// RowDiff represents the column-level differences for a single row. +type RowDiff struct { + Table string `json:"table"` + Key string `json:"key"` + Status RowDiffStatus `json:"status"` + Columns []ColumnDiff `json:"columns"` + DiffColumns []string `json:"differing_columns,omitempty"` +} + +// TableRowDiff represents all row differences for a specific table. +type TableRowDiff struct { + Table string `json:"table"` + Rows []RowDiff `json:"rows"` +} + +// RowDiffReport contains all row differences grouped by table. +type RowDiffReport struct { + Tables []TableRowDiff `json:"tables"` +} + +// HasChanges reports whether any row differences exist in the report. +func (r RowDiffReport) HasChanges() bool { + for _, t := range r.Tables { + if len(t.Rows) > 0 { + return true + } + } + return false +} + +// TotalRows returns the total count of differing rows in the report. +func (r RowDiffReport) TotalRows() int { + count := 0 + for _, t := range r.Tables { + count += len(t.Rows) + } + return count +} + +// RowDiffOptions controls which row diffs are fetched. +type RowDiffOptions struct { + TableFilter string // optional filter for a specific table + KeyFilter string // optional filter for a specific primary key + StatusFilter string // "updated", "added", "removed", or "all" (default: "updated") + Limit int // max rows to inspect per table (0 = unlimited) +} + +// FetchRowDiffReport fetches column-level row data and differences for conflicting / changed rows. +// It supports cross-engine setups where table or column casing differs between prod and dev. +func FetchRowDiffReport( + ctx context.Context, + prodDB, devDB *sql.DB, + prodDriver, devDriver string, + prodSchema, devSchema *schema.Schema, + dataDiff content.DataDiff, + conflicts content.Conflicts, + opts RowDiffOptions, +) (RowDiffReport, error) { + report := RowDiffReport{} + statusFilter := strings.ToLower(strings.TrimSpace(opts.StatusFilter)) + if statusFilter == "" { + statusFilter = string(StatusUpdated) + } + + includeUpdated := statusFilter == "all" || statusFilter == string(StatusUpdated) + includeAdded := statusFilter == "all" || statusFilter == string(StatusAdded) + includeRemoved := statusFilter == "all" || statusFilter == string(StatusRemoved) + + // Direct single-key lookup optimization when table and key are specified + if opts.TableFilter != "" && opts.KeyFilter != "" && len(dataDiff.Tables) == 0 { + conflict := content.Conflict{Table: opts.TableFilter, Key: opts.KeyFilter} + prod, dev, err := FetchConflictRowsCrossEngine( + ctx, prodDB, devDB, prodDriver, devDriver, prodSchema, devSchema, conflict, + ) + if err != nil { + return report, fmt.Errorf("fetch row data for %s key %s: %w", opts.TableFilter, opts.KeyFilter, err) + } + if prod != nil || dev != nil { + diffs := CompareRows(prod, dev) + var diffCols []string + for _, cd := range diffs { + if cd.Differs { + diffCols = append(diffCols, cd.Column) + } + } + st := StatusUpdated + if prod == nil { + st = StatusAdded + } else if dev == nil { + st = StatusRemoved + } + report.Tables = append(report.Tables, TableRowDiff{ + Table: opts.TableFilter, + Rows: []RowDiff{ + { + Table: opts.TableFilter, + Key: opts.KeyFilter, + Status: st, + Columns: diffs, + DiffColumns: diffCols, + }, + }, + }) + } + return report, nil + } + + for _, td := range dataDiff.Tables { + if opts.TableFilter != "" && schema.CanonicalIdent(td.Table) != schema.CanonicalIdent(opts.TableFilter) { + continue + } + + tableRows := TableRowDiff{Table: td.Table} + count := 0 + + // Helper to fetch and append a row diff + fetchRow := func(key string, status RowDiffStatus) error { + if opts.KeyFilter != "" && key != opts.KeyFilter { + return nil + } + if opts.Limit > 0 && count >= opts.Limit { + return nil + } + + conflict := content.Conflict{Table: td.Table, Key: key} + prod, dev, err := FetchConflictRowsCrossEngine( + ctx, prodDB, devDB, prodDriver, devDriver, prodSchema, devSchema, conflict, + ) + if err != nil { + return fmt.Errorf("fetch row %s.%s: %w", td.Table, key, err) + } + + diffs := CompareRows(prod, dev) + var diffCols []string + for _, cd := range diffs { + if cd.Differs { + diffCols = append(diffCols, cd.Column) + } + } + + tableRows.Rows = append(tableRows.Rows, RowDiff{ + Table: td.Table, + Key: key, + Status: status, + Columns: diffs, + DiffColumns: diffCols, + }) + count++ + return nil + } + + if includeUpdated { + for _, k := range td.Updated { + if err := fetchRow(k, StatusUpdated); err != nil { + return report, err + } + if opts.Limit > 0 && count >= opts.Limit { + break + } + } + } + + if includeRemoved { + for _, k := range td.Removed { + if err := fetchRow(k, StatusRemoved); err != nil { + return report, err + } + if opts.Limit > 0 && count >= opts.Limit { + break + } + } + } + + if includeAdded { + for _, k := range td.Added { + if err := fetchRow(k, StatusAdded); err != nil { + return report, err + } + if opts.Limit > 0 && count >= opts.Limit { + break + } + } + } + + if len(tableRows.Rows) > 0 { + report.Tables = append(report.Tables, tableRows) + } + } + + return report, nil +} + +// WriteRowDiffReport writes the row diff report to row_diff.json and row_diff.txt in outDir. +func WriteRowDiffReport(report RowDiffReport, outDir string) error { + if err := os.MkdirAll(outDir, 0o750); err != nil { + return fmt.Errorf("ensure output dir: %w", err) + } + + // 1. JSON + data, err := json.MarshalIndent(report, "", " ") + if err != nil { + return fmt.Errorf("marshal row diff json: %w", err) + } + if err := os.WriteFile(filepath.Join(outDir, "row_diff.json"), data, 0o600); err != nil { + return fmt.Errorf("write row_diff.json: %w", err) + } + + // 2. Text + txt := FormatRowDiffText(report) + if err := os.WriteFile(filepath.Join(outDir, "row_diff.txt"), []byte(txt), 0o600); err != nil { + return fmt.Errorf("write row_diff.txt: %w", err) + } + + return nil +} + +// FormatRowDiffText returns a human-readable text representation of row differences. +func FormatRowDiffText(report RowDiffReport) string { + var b strings.Builder + if !report.HasChanges() { + b.WriteString("No row-level differences found.\n") + return b.String() + } + + colWidth := 20 + valWidth := 24 + + for _, trd := range report.Tables { + for _, rd := range trd.Rows { + b.WriteString(strings.Repeat("=", 75) + "\n") + fmt.Fprintf(&b, "Table: %s | Key: %s (%s)\n", rd.Table, rd.Key, rd.Status) + b.WriteString(strings.Repeat("=", 75) + "\n") + fmt.Fprintf(&b, " %-*s | %-*s | %-*s\n", + colWidth, "Column", + valWidth, "Production", + valWidth, "Development") + fmt.Fprintf(&b, " %s+%s+%s\n", + strings.Repeat("-", colWidth+1), + strings.Repeat("-", valWidth+2), + strings.Repeat("-", valWidth+2)) + + for _, cd := range rd.Columns { + prodStr := FormatValue(cd.ProdVal) + devStr := FormatValue(cd.DevVal) + marker := "" + if cd.Differs { + marker = " *" + } + fmt.Fprintf(&b, " %-*s | %-*s | %-*s%s\n", + colWidth, cd.Column, + valWidth, prodStr, + valWidth, devStr, + marker) + } + + b.WriteString("\n * indicates columns that differ\n") + b.WriteString(strings.Repeat("=", 75) + "\n\n") + } + } + + return b.String() +} diff --git a/tests/resolve/fetch_test.go b/tests/resolve/fetch_test.go index f56450d..7d6de6a 100644 --- a/tests/resolve/fetch_test.go +++ b/tests/resolve/fetch_test.go @@ -564,3 +564,114 @@ func TestFetchConflictRows_NoPrimaryKey(t *testing.T) { t.Error("expected error for table with no primary key") } } + +// ============================================================================ +// Cross-engine case-insensitive table and column matching tests +// ============================================================================ + +func TestFetchConflictRowsCrossEngine_CaseInsensitiveTable(t *testing.T) { + ctx := context.Background() + prodDB := openFetchMemDB(t) + devDB := openFetchMemDB(t) + + mustExec(t, prodDB, `CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, email TEXT)`) + mustExec(t, prodDB, `INSERT INTO customers VALUES (1, 'Alice', 'alice@prod.com')`) + + mustExec(t, devDB, `CREATE TABLE CUSTOMERS (ID INTEGER PRIMARY KEY, NAME TEXT, EMAIL TEXT)`) + mustExec(t, devDB, `INSERT INTO CUSTOMERS VALUES (1, 'Alice', 'alice@dev.com')`) + + prodSch := &schema.Schema{ + Tables: map[string]schema.Table{ + "customers": { + Name: "customers", + Columns: map[string]schema.Column{ + "id": {Name: "id"}, + "name": {Name: "name"}, + "email": {Name: "email"}, + }, + PrimaryKey: []string{"id"}, + }, + }, + } + devSch := &schema.Schema{ + Tables: map[string]schema.Table{ + "CUSTOMERS": { + Name: "CUSTOMERS", + Columns: map[string]schema.Column{ + "ID": {Name: "ID"}, + "NAME": {Name: "NAME"}, + "EMAIL": {Name: "EMAIL"}, + }, + PrimaryKey: []string{"ID"}, + }, + }, + } + + conflict := content.Conflict{Table: "customers", Key: "1"} + prod, dev, err := resolve.FetchConflictRowsCrossEngine(ctx, prodDB, devDB, "sqlite", "sqlite", prodSch, devSch, conflict) + if err != nil { + t.Fatalf("FetchConflictRowsCrossEngine: %v", err) + } + if prod == nil || dev == nil { + t.Fatal("expected both prod and dev rows across case-differing table names") + } + + diffs := resolve.CompareRows(prod, dev) + if len(diffs) != 3 { + t.Fatalf("expected 3 column diffs, got %d", len(diffs)) + } + + for _, d := range diffs { + if d.Column == "email" { + if !d.Differs { + t.Errorf("expected email column to differ: prod=%v, dev=%v", d.ProdVal, d.DevVal) + } + } else { + if d.Differs { + t.Errorf("expected column %q to match across case-differing names, but marked differing", d.Column) + } + } + } +} + +func TestCompareRows_CrossEngineColumnCasing(t *testing.T) { + prod := &resolve.RowData{ + Columns: []string{"id", "name", "email", "status"}, + Values: map[string]any{ + "id": int64(42), + "name": "Dan", + "email": "dan@example.com", + "status": "ACTIVE", + }, + } + dev := &resolve.RowData{ + Columns: []string{"ID", "NAME", "EMAIL", "STATUS"}, + Values: map[string]any{ + "ID": int64(42), + "NAME": "Dan", + "EMAIL": "dan+new@example.com", + "STATUS": "ACTIVE", + }, + } + + diffs := resolve.CompareRows(prod, dev) + if len(diffs) != 4 { + t.Fatalf("expected 4 matched columns, got %d", len(diffs)) + } + + differingCount := 0 + var diffCol string + for _, d := range diffs { + if d.Differs { + differingCount++ + diffCol = d.Column + } + } + + if differingCount != 1 { + t.Fatalf("expected exactly 1 differing column, got %d", differingCount) + } + if diffCol != "email" { + t.Errorf("expected differing column to be 'email', got %q", diffCol) + } +} diff --git a/tests/resolve/rowdiff_test.go b/tests/resolve/rowdiff_test.go new file mode 100644 index 0000000..76aa14a --- /dev/null +++ b/tests/resolve/rowdiff_test.go @@ -0,0 +1,174 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/iamvirul/deepdiff-db/internal/content" + "github.com/iamvirul/deepdiff-db/internal/content/resolve" + "github.com/iamvirul/deepdiff-db/internal/schema" +) + +func TestFetchRowDiffReport_UpdatedAndRemovedAndAdded(t *testing.T) { + ctx := context.Background() + prodDB := openFetchMemDB(t) + devDB := openFetchMemDB(t) + + mustExec(t, prodDB, `CREATE TABLE items (id INTEGER PRIMARY KEY, title TEXT, price REAL)`) + mustExec(t, prodDB, `INSERT INTO items VALUES (1, 'Book', 10.5), (2, 'Pen', 1.5), (3, 'Notebook', 4.0)`) + + mustExec(t, devDB, `CREATE TABLE items (id INTEGER PRIMARY KEY, title TEXT, price REAL)`) + mustExec(t, devDB, `INSERT INTO items VALUES (1, 'Book', 12.0), (2, 'Pen', 1.5), (4, 'Eraser', 0.5)`) + + prodSch := &schema.Schema{ + Tables: map[string]schema.Table{ + "items": { + Name: "items", + Columns: map[string]schema.Column{ + "id": {Name: "id"}, + "title": {Name: "title"}, + "price": {Name: "price"}, + }, + PrimaryKey: []string{"id"}, + }, + }, + } + devSch := &schema.Schema{ + Tables: map[string]schema.Table{ + "items": { + Name: "items", + Columns: map[string]schema.Column{ + "id": {Name: "id"}, + "title": {Name: "title"}, + "price": {Name: "price"}, + }, + PrimaryKey: []string{"id"}, + }, + }, + } + + dataDiff := content.DataDiff{ + Tables: []content.TableDataDiff{ + { + Table: "items", + Updated: []string{"1"}, + Removed: []string{"3"}, + Added: []string{"4"}, + }, + }, + } + conflicts := content.Conflicts{ + Conflicts: []content.Conflict{ + {Table: "items", Key: "1"}, + }, + } + + // 1. Test status "all" + opts := resolve.RowDiffOptions{StatusFilter: "all"} + report, err := resolve.FetchRowDiffReport(ctx, prodDB, devDB, "sqlite", "sqlite", prodSch, devSch, dataDiff, conflicts, opts) + if err != nil { + t.Fatalf("FetchRowDiffReport: %v", err) + } + + if len(report.Tables) != 1 { + t.Fatalf("expected 1 table, got %d", len(report.Tables)) + } + if len(report.Tables[0].Rows) != 3 { + t.Fatalf("expected 3 row diffs, got %d", len(report.Tables[0].Rows)) + } + + // Check updated row (key 1) + row1 := report.Tables[0].Rows[0] + if row1.Key != "1" || row1.Status != resolve.StatusUpdated { + t.Errorf("expected row 1 to be updated, got %+v", row1) + } + if len(row1.DiffColumns) != 1 || row1.DiffColumns[0] != "price" { + t.Errorf("expected price to be differing column for row 1, got %v", row1.DiffColumns) + } + + // Check removed row (key 3) + row3 := report.Tables[0].Rows[1] + if row3.Key != "3" || row3.Status != resolve.StatusRemoved { + t.Errorf("expected row 3 to be removed, got %+v", row3) + } + + // Check added row (key 4) + row4 := report.Tables[0].Rows[2] + if row4.Key != "4" || row4.Status != resolve.StatusAdded { + t.Errorf("expected row 4 to be added, got %+v", row4) + } + + // 2. Test status "updated" only + optsUpdated := resolve.RowDiffOptions{StatusFilter: "updated"} + reportUpdated, err := resolve.FetchRowDiffReport(ctx, prodDB, devDB, "sqlite", "sqlite", prodSch, devSch, dataDiff, conflicts, optsUpdated) + if err != nil { + t.Fatalf("FetchRowDiffReport (updated): %v", err) + } + if len(reportUpdated.Tables[0].Rows) != 1 || reportUpdated.Tables[0].Rows[0].Key != "1" { + t.Errorf("expected only key 1 for updated filter, got %d rows", len(reportUpdated.Tables[0].Rows)) + } + + // 3. Test key filter + optsKey := resolve.RowDiffOptions{StatusFilter: "all", KeyFilter: "3"} + reportKey, err := resolve.FetchRowDiffReport(ctx, prodDB, devDB, "sqlite", "sqlite", prodSch, devSch, dataDiff, conflicts, optsKey) + if err != nil { + t.Fatalf("FetchRowDiffReport (key filter): %v", err) + } + if len(reportKey.Tables[0].Rows) != 1 || reportKey.Tables[0].Rows[0].Key != "3" { + t.Errorf("expected only key 3 for key filter, got %d rows", len(reportKey.Tables[0].Rows)) + } +} + +func TestWriteRowDiffReport_AndFormat(t *testing.T) { + report := resolve.RowDiffReport{ + Tables: []resolve.TableRowDiff{ + { + Table: "customers", + Rows: []resolve.RowDiff{ + { + Table: "customers", + Key: "4", + Status: resolve.StatusUpdated, + Columns: []resolve.ColumnDiff{ + {Column: "id", ProdVal: 4, DevVal: 4, Differs: false}, + {Column: "email", ProdVal: "dan@example.com", DevVal: "dan+new@example.com", Differs: true}, + }, + DiffColumns: []string{"email"}, + }, + }, + }, + }, + } + + tmpDir, err := os.MkdirTemp("", "rowdiff_test_*") + if err != nil { + t.Fatalf("MkdirTemp: %v", err) + } + defer os.RemoveAll(tmpDir) + + if err := resolve.WriteRowDiffReport(report, tmpDir); err != nil { + t.Fatalf("WriteRowDiffReport: %v", err) + } + + jsonPath := filepath.Join(tmpDir, "row_diff.json") + if _, err := os.Stat(jsonPath); err != nil { + t.Errorf("row_diff.json was not created: %v", err) + } + + txtPath := filepath.Join(tmpDir, "row_diff.txt") + txtData, err := os.ReadFile(txtPath) + if err != nil { + t.Fatalf("read row_diff.txt: %v", err) + } + + txt := string(txtData) + if !strings.Contains(txt, "Table: customers | Key: 4 (updated)") { + t.Errorf("row_diff.txt missing table/key header: %s", txt) + } + if !strings.Contains(txt, "dan+new@example.com") { + t.Errorf("row_diff.txt missing updated value: %s", txt) + } +} From 58174f5715c40f8272c1766c94f6cdd8ddede054 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Fri, 25 Sep 2026 08:56:47 +0530 Subject: [PATCH 2/7] feat(html): display column-level row data diffs in HTML report --- cmd/deepdiffdb/main.go | 35 ++-- internal/report/html/generator.go | 47 +++++- internal/report/html/template.go | 258 ++++++++++++++++++++++++++---- internal/report/html/types.go | 31 ++-- 4 files changed, 319 insertions(+), 52 deletions(-) diff --git a/cmd/deepdiffdb/main.go b/cmd/deepdiffdb/main.go index cb5d48a..f243dfa 100644 --- a/cmd/deepdiffdb/main.go +++ b/cmd/deepdiffdb/main.go @@ -466,33 +466,37 @@ func runFullDiff(args []string) error { log.Info("full diff complete", "tables_scanned", tablesScanned, "has_changes", dataDiff.HasChanges()) fmt.Println("Schema OK. Data diff complete.") + var rowReport *resolve.RowDiffReport if dataDiff.HasChanges() { fmt.Printf("Changes detected. See %s, %s, and %s\n", filepath.Join(cfg.Output.Dir, "content_diff.json"), filepath.Join(cfg.Output.Dir, "conflicts.json"), filepath.Join(cfg.Output.Dir, "summary.txt")) if conflicts.HasConflicts() { fmt.Printf("Warning: %d conflicts detected. Review %s\n", len(conflicts.Conflicts), filepath.Join(cfg.Output.Dir, "conflicts.json")) } - if *showRowDiff { + if *showRowDiff || *generateHTML { log.Info("fetching column-level row differences") - rowReport, err := resolve.FetchRowDiffReport( + fetchedReport, err := resolve.FetchRowDiffReport( ctx, prodDB, devDB, cfg.Prod.Driver, cfg.Dev.Driver, prodSchema, devSchema, dataDiff, conflicts, resolve.RowDiffOptions{ - StatusFilter: "updated", + StatusFilter: "all", }, ) if err != nil { log.Warn("failed to fetch row diffs", "error", err) - } else if rowReport.HasChanges() { - if err := resolve.WriteRowDiffReport(rowReport, cfg.Output.Dir); err != nil { - log.Warn("failed to write row diff report", "error", err) - } - display := cli.NewDisplay() - for _, trd := range rowReport.Tables { - for _, rd := range trd.Rows { - display.PrintRowDiff(rd.Table, rd.Key, string(rd.Status), rd.Columns) + } else if fetchedReport.HasChanges() { + rowReport = &fetchedReport + if *showRowDiff { + if err := resolve.WriteRowDiffReport(*rowReport, cfg.Output.Dir); err != nil { + log.Warn("failed to write row diff report", "error", err) + } + display := cli.NewDisplay() + for _, trd := range rowReport.Tables { + for _, rd := range trd.Rows { + display.PrintRowDiff(rd.Table, rd.Key, string(rd.Status), rd.Columns) + } } + fmt.Printf("Row-level column diffs written to %s and %s\n", filepath.Join(cfg.Output.Dir, "row_diff.json"), filepath.Join(cfg.Output.Dir, "row_diff.txt")) } - fmt.Printf("Row-level column diffs written to %s and %s\n", filepath.Join(cfg.Output.Dir, "row_diff.json"), filepath.Join(cfg.Output.Dir, "row_diff.txt")) } } } else { @@ -502,6 +506,9 @@ func runFullDiff(args []string) error { // Generate HTML report if requested if *generateHTML { htmlPath := filepath.Join(cfg.Output.Dir, "report.html") + reportOpts := htmlreport.DefaultReportOptions() + reportOpts.RowDiffReport = rowReport + reportData := htmlreport.BuildReportData( fmt.Sprintf("%s:%d/%s", cfg.Prod.Host, cfg.Prod.Port, cfg.Prod.Database), fmt.Sprintf("%s:%d/%s", cfg.Dev.Host, cfg.Dev.Port, cfg.Dev.Database), @@ -513,11 +520,11 @@ func runFullDiff(args []string) error { "", // No migration SQL for diff command "", // No migration pack for diff command tablesScanned, - nil, + reportOpts, ) reportData.Version = version - generator := htmlreport.NewGenerator(nil) + generator := htmlreport.NewGenerator(reportOpts) if err := generator.GenerateReport(reportData, htmlPath); err != nil { return fmt.Errorf("generate HTML report: %w", err) } diff --git a/internal/report/html/generator.go b/internal/report/html/generator.go index fe9852a..e2bfd92 100644 --- a/internal/report/html/generator.go +++ b/internal/report/html/generator.go @@ -63,6 +63,9 @@ func templateFuncs() template.FuncMap { "join": func(items []string, sep string) string { return strings.Join(items, sep) }, + "formatVal": func(v any) string { + return resolve.FormatValue(v) + }, "formatTime": func(t time.Time) string { return t.Format("2006-01-02 15:04:05 MST") }, @@ -175,11 +178,17 @@ func BuildReportData( data.TableDiffs = buildTableDiffs(dataDiff, opts) } + // Process row diff report + if opts.RowDiffReport != nil && opts.RowDiffReport.HasChanges() { + data.RowDiffReport = opts.RowDiffReport + data.HasRowDiffs = true + } + // Process conflicts with resolution details if conflicts != nil && conflicts.HasConflicts() { data.Conflicts = conflicts data.HasConflicts = true - data.ConflictItems = buildConflictItemsWithResolutions(conflicts, resolutions) + data.ConflictItems = buildConflictItemsWithResolutionsAndRowDiffs(conflicts, resolutions, opts.RowDiffReport) } // Process resolutions and build breakdown @@ -438,6 +447,16 @@ func buildTableDiffs(dataDiff *content.DataDiff, opts *ReportOptions) []TableDif td.UpdatedKeys = limitKeys(t.Updated, maxKeys) } + if opts.RowDiffReport != nil { + for _, trd := range opts.RowDiffReport.Tables { + if schema.CanonicalIdent(trd.Table) == schema.CanonicalIdent(t.Table) { + td.RowDiffs = trd.Rows + td.HasRowDiffs = len(trd.Rows) > 0 + break + } + } + } + diffs = append(diffs, td) } @@ -451,6 +470,11 @@ func buildTableDiffs(dataDiff *content.DataDiff, opts *ReportOptions) []TableDif // buildConflictItemsWithResolutions converts conflicts to display format with resolution details. func buildConflictItemsWithResolutions(conflicts *content.Conflicts, resolutions []resolve.Resolution) []ConflictDisplay { + return buildConflictItemsWithResolutionsAndRowDiffs(conflicts, resolutions, nil) +} + +// buildConflictItemsWithResolutionsAndRowDiffs converts conflicts to display format with resolution and row diff details. +func buildConflictItemsWithResolutionsAndRowDiffs(conflicts *content.Conflicts, resolutions []resolve.Resolution, rowReport *resolve.RowDiffReport) []ConflictDisplay { var items []ConflictDisplay // Build a map of resolutions by table+key for quick lookup @@ -460,6 +484,17 @@ func buildConflictItemsWithResolutions(conflicts *content.Conflicts, resolutions resolutionMap[key] = r } + // Build a map of row diffs by table+key for quick lookup + rowDiffMap := make(map[string]resolve.RowDiff) + if rowReport != nil { + for _, t := range rowReport.Tables { + for _, rd := range t.Rows { + rowDiffMap[rd.Table+":"+rd.Key] = rd + rowDiffMap[schema.CanonicalIdent(rd.Table)+":"+rd.Key] = rd + } + } + } + for _, c := range conflicts.Conflicts { item := ConflictDisplay{ Table: c.Table, @@ -477,6 +512,16 @@ func buildConflictItemsWithResolutions(conflicts *content.Conflicts, resolutions item.IsResolved = res.Resolved } + if rd, ok := rowDiffMap[key]; ok { + item.ColumnDiffs = rd.Columns + item.DiffColumns = rd.DiffColumns + item.HasColumnDiffs = len(rd.Columns) > 0 + } else if rd, ok := rowDiffMap[schema.CanonicalIdent(c.Table)+":"+c.Key]; ok { + item.ColumnDiffs = rd.Columns + item.DiffColumns = rd.DiffColumns + item.HasColumnDiffs = len(rd.Columns) > 0 + } + items = append(items, item) } diff --git a/internal/report/html/template.go b/internal/report/html/template.go index f3e5d9b..1650735 100644 --- a/internal/report/html/template.go +++ b/internal/report/html/template.go @@ -592,6 +592,15 @@ const reportTemplate = ` overflow: hidden; } + .conflict-item-card { + border-bottom: 1px solid var(--border); + background: var(--bg); + } + + .conflict-item-card:last-child { + border-bottom: none; + } + .conflict-item { display: grid; grid-template-columns: 140px 1fr 200px 100px; @@ -600,12 +609,78 @@ const reportTemplate = ` padding: 12px 16px; background: var(--bg); font-size: 13px; + cursor: pointer; } .conflict-item:hover { background: var(--bg-secondary); } + .conflict-item-body { + padding: 0 16px 14px 16px; + background: var(--bg-secondary); + border-top: 1px dashed var(--border); + } + + .row-data-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + margin-top: 10px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 4px; + overflow: hidden; + } + + .row-data-table th, .row-data-table td { + padding: 7px 12px; + text-align: left; + border-bottom: 1px solid var(--border); + } + + .row-data-table th { + background: var(--bg-tertiary); + font-weight: 600; + color: var(--text-secondary); + } + + .row-data-table tr:last-child td { + border-bottom: none; + } + + .diff-row-changed { + background: rgba(234, 88, 12, 0.08); + } + + .diff-star { + color: #ea580c; + font-weight: bold; + } + + .rowdiff-card { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + margin-bottom: 16px; + overflow: hidden; + } + + .rowdiff-header { + display: flex; + align-items: center; + gap: 16px; + padding: 10px 16px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + font-size: 13px; + font-weight: 500; + } + + .rowdiff-body { + padding: 12px 16px; + } + .conflict-table { font-family: var(--font-mono); font-weight: 500; @@ -873,21 +948,26 @@ const reportTemplate = `
-
+
{{if .HasSchemaDiff}} {{range .SchemaChanges}}
@@ -1035,7 +1115,7 @@ const reportTemplate = ` {{end}}
-
+
{{if .HasDataDiff}}
+ + +
+
+ {{range .RowDiffReport.Tables}} + {{range .Rows}} +
+
+ {{.Table}} + Key: {{.Key}} + {{.Status}} +
+
+ + + + + + + + + + {{range .Columns}} + + + + + + {{end}} + +
ColumnProductionDevelopment
{{.Column}}{{if .Differs}} *{{end}}{{formatVal .ProdVal}}{{formatVal .DevVal}}
+
+
+ {{end}} + {{end}} +
+
+ {{end}} + {{if .HasMigration}}
@@ -1185,8 +1366,17 @@ const reportTemplate = ` function filterConflicts() { const v = document.getElementById('conflict-filter').value; - document.querySelectorAll('#conflict-list .conflict-item').forEach(r => { - r.style.display = !v || r.dataset.table === v ? '' : 'none'; + document.querySelectorAll('#conflict-list .conflict-item-card').forEach(r => { + const show = !v || r.dataset.table === v; + r.style.display = show ? '' : 'none'; + }); + } + + function filterRowDiffs() { + const v = document.getElementById('rowdiff-filter').value; + document.querySelectorAll('#rowdiff-list .rowdiff-card').forEach(r => { + const show = !v || r.dataset.table === v; + r.style.display = show ? '' : 'none'; }); } @@ -1216,9 +1406,21 @@ const reportTemplate = ` const cf = document.getElementById('conflict-filter'); if (cf) { const ts = new Set(); - document.querySelectorAll('#conflict-list .conflict-item').forEach(i => ts.add(i.dataset.table)); + document.querySelectorAll('#conflict-list .conflict-item-card').forEach(i => ts.add(i.dataset.table)); ts.forEach(t => { const o = document.createElement('option'); o.value = t; o.textContent = t; cf.appendChild(o); }); } + + const rf = document.getElementById('rowdiff-filter'); + if (rf) { + const rts = new Set(); + document.querySelectorAll('#rowdiff-list .rowdiff-card').forEach(i => rts.add(i.dataset.table)); + rts.forEach(t => { const o = document.createElement('option'); o.value = t; o.textContent = t; rf.appendChild(o); }); + } + + const hash = window.location.hash.replace('#', ''); + if (hash && document.getElementById('tab-' + hash)) { + switchTab(hash); + } }); diff --git a/internal/report/html/types.go b/internal/report/html/types.go index 01e3730..9d874a4 100644 --- a/internal/report/html/types.go +++ b/internal/report/html/types.go @@ -4,6 +4,7 @@ import ( "time" "github.com/iamvirul/deepdiff-db/internal/content" + "github.com/iamvirul/deepdiff-db/internal/content/resolve" "github.com/iamvirul/deepdiff-db/internal/schema" ) @@ -91,6 +92,10 @@ type ReportData struct { // SQL Migration MigrationSQL string `json:"migration_sql,omitempty"` HasMigration bool `json:"has_migration"` + + // Row data differences + RowDiffReport *resolve.RowDiffReport `json:"row_diff_report,omitempty"` + HasRowDiffs bool `json:"has_row_diffs"` } // ReportSummary contains aggregate statistics. @@ -160,14 +165,16 @@ type ForeignKeyChangeDisplay struct { // TableDiffDisplay represents a table's data differences for display. type TableDiffDisplay struct { - Table string `json:"table"` - AddedCount int `json:"added_count"` - RemovedCount int `json:"removed_count"` - UpdatedCount int `json:"updated_count"` - AddedKeys []string `json:"added_keys,omitempty"` - RemovedKeys []string `json:"removed_keys,omitempty"` - UpdatedKeys []string `json:"updated_keys,omitempty"` - HasChanges bool `json:"has_changes"` + Table string `json:"table"` + AddedCount int `json:"added_count"` + RemovedCount int `json:"removed_count"` + UpdatedCount int `json:"updated_count"` + AddedKeys []string `json:"added_keys,omitempty"` + RemovedKeys []string `json:"removed_keys,omitempty"` + UpdatedKeys []string `json:"updated_keys,omitempty"` + HasChanges bool `json:"has_changes"` + RowDiffs []resolve.RowDiff `json:"row_diffs,omitempty"` + HasRowDiffs bool `json:"has_row_diffs"` } // ConflictDisplay represents a conflict for display. @@ -179,7 +186,10 @@ type ConflictDisplay struct { Resolution string `json:"resolution,omitempty"` // "keep_prod", "use_dev", "pending" Strategy string `json:"strategy,omitempty"` // "ours", "theirs", "manual" Decision string `json:"decision,omitempty"` // "keep_prod", "use_dev", "pending" - IsResolved bool `json:"is_resolved"` + IsResolved bool `json:"is_resolved"` + ColumnDiffs []resolve.ColumnDiff `json:"column_diffs,omitempty"` + DiffColumns []string `json:"differing_columns,omitempty"` + HasColumnDiffs bool `json:"has_column_diffs"` } // TableStrategyDisplay shows the resolution strategy for a table. @@ -213,6 +223,9 @@ type ReportOptions struct { // Enable PDF export button (requires browser print) EnablePDFExport bool + + // RowDiffReport contains column-level row differences for display + RowDiffReport *resolve.RowDiffReport } // DefaultReportOptions returns sensible defaults for report generation. From 0be60d8a9e1af691d8afb049a25dfecd7df63525 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Fri, 25 Sep 2026 09:05:55 +0530 Subject: [PATCH 3/7] docs: add row-diff command, cross-engine diff guide with pipeline mismatch image, and sample 18 --- samples/18-cross-engine-row-diff/Makefile | 51 + samples/18-cross-engine-row-diff/README.md | 138 +++ .../deepdiffdb.config.yaml | 32 + .../docker-compose.yml | 31 + .../init-scripts/01-postgres-init.sql | 39 + .../init-scripts/02-oracle-init.sql | 55 + website/docs/commands/diff.md | 8 +- website/docs/commands/index.md | 1 + website/docs/commands/row-diff.md | 116 ++ website/docs/features/cross-engine-diff.md | 130 ++ website/docs/features/html-reports.md | 21 +- website/docs/samples/index.md | 16 + website/sidebars.js | 4 +- .../img/cross-engine-pipeline-mismatch.jpg | Bin 0 -> 141258 bytes website/static/img/row-diff-terminal.svg | 97 ++ website/static/samples/report.html | 1041 ++++++++--------- 16 files changed, 1198 insertions(+), 582 deletions(-) create mode 100644 samples/18-cross-engine-row-diff/Makefile create mode 100644 samples/18-cross-engine-row-diff/README.md create mode 100644 samples/18-cross-engine-row-diff/deepdiffdb.config.yaml create mode 100644 samples/18-cross-engine-row-diff/docker-compose.yml create mode 100644 samples/18-cross-engine-row-diff/init-scripts/01-postgres-init.sql create mode 100644 samples/18-cross-engine-row-diff/init-scripts/02-oracle-init.sql create mode 100644 website/docs/commands/row-diff.md create mode 100644 website/docs/features/cross-engine-diff.md create mode 100644 website/static/img/cross-engine-pipeline-mismatch.jpg create mode 100644 website/static/img/row-diff-terminal.svg diff --git a/samples/18-cross-engine-row-diff/Makefile b/samples/18-cross-engine-row-diff/Makefile new file mode 100644 index 0000000..e7806b9 --- /dev/null +++ b/samples/18-cross-engine-row-diff/Makefile @@ -0,0 +1,51 @@ +BIN = ../../deepdiffdb + +.PHONY: help up wait-healthy seed diff row-diff html down clean + +help: + @echo "Sample 18: Cross-Engine Row Diff & Migration Verification" + @echo "" + @echo "Targets:" + @echo " make up - Start PostgreSQL 16 and Oracle XE 21c containers" + @echo " make wait-healthy - Wait for both containers to report healthy" + @echo " make seed - Initialize schemas and seed drifted data" + @echo " make diff - Run full cross-engine diff (schema + data)" + @echo " make row-diff - Inspect column-level differences in terminal" + @echo " make html - Generate and open interactive HTML report" + @echo " make down - Stop and remove Docker containers" + @echo " make clean - Remove diff-output artifacts" + +up: + docker compose up -d + +wait-healthy: + @echo "Waiting for PostgreSQL to be healthy..." + @until [ "$$(docker inspect --format='{{.State.Health.Status}}' sample18-postgres 2>/dev/null)" = "healthy" ]; do sleep 2; done + @echo "PostgreSQL is ready." + @echo "Waiting for Oracle to be healthy (takes ~45-90s on initial start)..." + @until [ "$$(docker inspect --format='{{.State.Health.Status}}' sample18-oracle 2>/dev/null)" = "healthy" ]; do sleep 5; done + @echo "Oracle XE is ready." + +seed: + @echo "Seeding PostgreSQL..." + docker exec -i sample18-postgres psql -U postgres -d appdb < init-scripts/01-postgres-init.sql + @echo "Seeding Oracle XE..." + docker exec -i sample18-oracle sqlplus -S appuser/secret@XEPDB1 < init-scripts/02-oracle-init.sql + @echo "Seeding complete." + +diff: + $(BIN) diff --config deepdiffdb.config.yaml + +row-diff: + $(BIN) row-diff --config deepdiffdb.config.yaml --status all + +html: + $(BIN) diff --config deepdiffdb.config.yaml --html --row-diff + @echo "Opening diff-output/report.html..." + @open diff-output/report.html 2>/dev/null || xdg-open diff-output/report.html 2>/dev/null || echo "Report available at diff-output/report.html" + +down: + docker compose down -v + +clean: + rm -rf diff-output diff --git a/samples/18-cross-engine-row-diff/README.md b/samples/18-cross-engine-row-diff/README.md new file mode 100644 index 0000000..e6144f6 --- /dev/null +++ b/samples/18-cross-engine-row-diff/README.md @@ -0,0 +1,138 @@ +# Sample 18: Cross-Engine Row Diff & Migration Verification + +This sample demonstrates **cross-engine migration verification** between **PostgreSQL 16** (Source / Production) and **Oracle XE 21c** (Target / Development). + +It replicates common real-world migration discrepancies where: +1. **Identifier case differs**: PostgreSQL stores unquoted identifiers in lowercase (`um_user`, `um_user_attribute`), whereas Oracle stores them in UPPERCASE (`UM_USER`, `UM_USER_ATTRIBUTE`). +2. **Data drift exists**: Certain rows have modified columns, missing records, or newly inserted rows. +3. **Column-level inspection is needed**: You need to see exactly *which* column drifted (e.g. `email` or `attr_value`) rather than just knowing that a row checksum failed. + +--- + +## Architecture & Scenario + +```text +┌─────────────────────────────────┐ ┌─────────────────────────────────┐ +│ PostgreSQL 16 (Prod) │ │ Oracle XE 21c (Dev) │ +│ Port: 55432 │ │ Port: 51521 │ +├─────────────────────────────────┤ ├─────────────────────────────────┤ +│ um_user (6 rows, lowercase) │ ◄───────► │ UM_USER (5 rows, UPPERCASE) │ +│ um_user_attribute (7 rows) │ │ UM_USER_ATTRIBUTE (6 rows) │ +└─────────────────────────────────┘ └─────────────────────────────────┘ + │ + ▼ + ┌───────────────────────────┐ + │ DeepDiff DB │ + │ Canonical Case Folding │ + │ Column-Level Row Diffing │ + └───────────────────────────┘ +``` + +--- + +## Quick Start + +### 1. Start Containers + +```bash +make up +make wait-healthy +``` + +*Note: Oracle XE 21c takes approximately 45–90 seconds to initialize on the first launch.* + +### 2. Seed Data + +```bash +make seed +``` + +This script seeds: +- **`um_user` / `UM_USER`**: + - Key `1`, `2`, `3`: Identical in both databases. + - Key `4` (`dan`): Email in Postgres is `dan@corp.internal`, but in Oracle is `dan+updated@corp.internal`. + - Keys `5`, `6` (`eve`, `frank`): Present in Postgres, missing in Oracle. + - Key `7` (`grace`): Present in Oracle, missing in Postgres. +- **`um_user_attribute` / `UM_USER_ATTRIBUTE`**: + - Key `104`: Attribute `department` is `'Finance'` in Postgres, but `'Accounting'` in Oracle. + - Keys `106`, `107`: Missing in Oracle. + - Key `108`: New in Oracle. + +### 3. Run Cross-Engine Diff + +```bash +make diff +``` + +DeepDiff DB canonically pairs `um_user` with `UM_USER` and `um_user_attribute` with `UM_USER_ATTRIBUTE`: + +```text +Changes detected. See diff-output/content_diff.json, diff-output/conflicts.json, and diff-output/summary.txt +Warning: 2 conflicts detected. Review diff-output/conflicts.json +``` + +### 4. Inspect Column-Level Row Differences (`row-diff`) + +Run `make row-diff` to view the exact column values side-by-side: + +```bash +make row-diff +``` + +Output: + +```text +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Table: um_user | Key: 4 (updated) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Column | Production (PostgreSQL) | Development (Oracle) + ---------------------+--------------------------+-------------------------- + email | dan@corp.internal | dan+updated@corp.internal * + id | 4 | 4 + status | ACTIVE | ACTIVE + username | dan | dan + + * indicates columns that differ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Table: um_user_attribute | Key: 104 (updated) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Column | Production (PostgreSQL) | Development (Oracle) + ---------------------+--------------------------+-------------------------- + attr_name | department | department + attr_value | Finance | Accounting * + id | 104 | 104 + user_id | 3 | 3 + + * indicates columns that differ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 5. Targeted Key Inspection + +To inspect a single record directly without scanning the whole table: + +```bash +../../deepdiffdb row-diff --table um_user --key 4 +``` + +### 6. Interactive HTML Report + +Generate the HTML viewer with column diffing embedded: + +```bash +make html +``` + +The report opens in your browser at `diff-output/report.html`, featuring: +- **Data Changes Tab**: Click `um_user` or `um_user_attribute` to see the column diff tables right inside the expanded row. +- **Row Data Tab**: Browse all changed records with real-time table filtering. +- **Conflicts Tab**: Inspect conflicting hashes with side-by-side values. + +### 7. Teardown + +```bash +make down +make clean +``` diff --git a/samples/18-cross-engine-row-diff/deepdiffdb.config.yaml b/samples/18-cross-engine-row-diff/deepdiffdb.config.yaml new file mode 100644 index 0000000..580a152 --- /dev/null +++ b/samples/18-cross-engine-row-diff/deepdiffdb.config.yaml @@ -0,0 +1,32 @@ +# DeepDiff DB Configuration: PostgreSQL (Prod) <-> Oracle (Dev) +# Demonstrates cross-engine case-insensitive table discovery and row diffing. + +prod: + driver: postgres + host: localhost + port: 55432 + user: postgres + password: secret + database: appdb + ssl_mode: disable + +dev: + driver: oracle + host: localhost + port: 51521 + user: appuser + password: secret + database: XEPDB1 + +ignore: + tables: [] + columns: + - created_at + - CREATED_AT + +performance: + hash_batch_size: 1000 + max_parallel_tables: 4 + +output: + dir: ./diff-output diff --git a/samples/18-cross-engine-row-diff/docker-compose.yml b/samples/18-cross-engine-row-diff/docker-compose.yml new file mode 100644 index 0000000..7bb7aaa --- /dev/null +++ b/samples/18-cross-engine-row-diff/docker-compose.yml @@ -0,0 +1,31 @@ +services: + postgres: + image: postgres:16-alpine + container_name: sample18-postgres + environment: + POSTGRES_DB: appdb + POSTGRES_USER: postgres + POSTGRES_PASSWORD: secret + ports: + - "55432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"] + interval: 5s + timeout: 5s + retries: 5 + + oracle: + image: gvenzl/oracle-xe:21-slim-faststart + container_name: sample18-oracle + environment: + ORACLE_PASSWORD: secret + APP_USER: appuser + APP_USER_PASSWORD: secret + ports: + - "51521:1521" + healthcheck: + test: ["CMD-SHELL", "/opt/oracle/healthcheck.sh"] + interval: 10s + timeout: 10s + retries: 20 + start_period: 45s diff --git a/samples/18-cross-engine-row-diff/init-scripts/01-postgres-init.sql b/samples/18-cross-engine-row-diff/init-scripts/01-postgres-init.sql new file mode 100644 index 0000000..5594884 --- /dev/null +++ b/samples/18-cross-engine-row-diff/init-scripts/01-postgres-init.sql @@ -0,0 +1,39 @@ +-- PostgreSQL initialization (Source / Production) +-- Default PostgreSQL table and column identifiers are folded to lowercase. + +DROP TABLE IF EXISTS um_user_attribute; +DROP TABLE IF EXISTS um_user; + +CREATE TABLE um_user ( + id SERIAL PRIMARY KEY, + username VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE um_user_attribute ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES um_user(id) ON DELETE CASCADE, + attr_name VARCHAR(100) NOT NULL, + attr_value VARCHAR(255) NOT NULL +); + +-- Seed production users +INSERT INTO um_user (id, username, email, status) VALUES +(1, 'alice', 'alice@corp.internal', 'ACTIVE'), +(2, 'bob', 'bob@corp.internal', 'ACTIVE'), +(3, 'carol', 'carol@corp.internal', 'INACTIVE'), +(4, 'dan', 'dan@corp.internal', 'ACTIVE'), +(5, 'eve', 'eve@corp.internal', 'ACTIVE'), +(6, 'frank', 'frank@corp.internal', 'ACTIVE'); + +-- Seed user attributes +INSERT INTO um_user_attribute (id, user_id, attr_name, attr_value) VALUES +(101, 1, 'department', 'Engineering'), +(102, 1, 'role', 'Lead Developer'), +(103, 2, 'department', 'DevOps'), +(104, 3, 'department', 'Finance'), +(105, 4, 'department', 'Security'), +(106, 5, 'department', 'HR'), +(107, 6, 'department', 'Sales'); diff --git a/samples/18-cross-engine-row-diff/init-scripts/02-oracle-init.sql b/samples/18-cross-engine-row-diff/init-scripts/02-oracle-init.sql new file mode 100644 index 0000000..02cf8b5 --- /dev/null +++ b/samples/18-cross-engine-row-diff/init-scripts/02-oracle-init.sql @@ -0,0 +1,55 @@ +-- Oracle initialization (Target / Development) +-- Default Oracle table and column identifiers are folded to UPPERCASE. + +BEGIN + EXECUTE IMMEDIATE 'DROP TABLE UM_USER_ATTRIBUTE CASCADE CONSTRAINTS'; +EXCEPTION + WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; +END; +/ + +BEGIN + EXECUTE IMMEDIATE 'DROP TABLE UM_USER CASCADE CONSTRAINTS'; +EXCEPTION + WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; +END; +/ + +CREATE TABLE UM_USER ( + ID NUMBER(10) PRIMARY KEY, + USERNAME VARCHAR2(100) NOT NULL, + EMAIL VARCHAR2(255) NOT NULL, + STATUS VARCHAR2(20) DEFAULT 'ACTIVE' NOT NULL, + CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +CREATE TABLE UM_USER_ATTRIBUTE ( + ID NUMBER(10) PRIMARY KEY, + USER_ID NUMBER(10) NOT NULL REFERENCES UM_USER(ID), + ATTR_NAME VARCHAR2(100) NOT NULL, + ATTR_VALUE VARCHAR2(255) NOT NULL +); + +-- Seed development users with intentional drift: +-- Row 1, 2, 3: identical +-- Row 4: updated email ('dan+updated@corp.internal' vs 'dan@corp.internal') +-- Row 5, 6: missing (deleted or omitted during partial sync) +-- Row 7: new user added in target +INSERT INTO UM_USER (ID, USERNAME, EMAIL, STATUS) VALUES (1, 'alice', 'alice@corp.internal', 'ACTIVE'); +INSERT INTO UM_USER (ID, USERNAME, EMAIL, STATUS) VALUES (2, 'bob', 'bob@corp.internal', 'ACTIVE'); +INSERT INTO UM_USER (ID, USERNAME, EMAIL, STATUS) VALUES (3, 'carol', 'carol@corp.internal', 'INACTIVE'); +INSERT INTO UM_USER (ID, USERNAME, EMAIL, STATUS) VALUES (4, 'dan', 'dan+updated@corp.internal', 'ACTIVE'); +INSERT INTO UM_USER (ID, USERNAME, EMAIL, STATUS) VALUES (7, 'grace', 'grace@corp.internal', 'ACTIVE'); + +-- Seed user attributes with drift: +-- Attr 104: updated attr_value ('Accounting' vs 'Finance') +-- Attr 106, 107: missing +-- Attr 108: new attribute for grace +INSERT INTO UM_USER_ATTRIBUTE (ID, USER_ID, ATTR_NAME, ATTR_VALUE) VALUES (101, 1, 'department', 'Engineering'); +INSERT INTO UM_USER_ATTRIBUTE (ID, USER_ID, ATTR_NAME, ATTR_VALUE) VALUES (102, 1, 'role', 'Lead Developer'); +INSERT INTO UM_USER_ATTRIBUTE (ID, USER_ID, ATTR_NAME, ATTR_VALUE) VALUES (103, 2, 'department', 'DevOps'); +INSERT INTO UM_USER_ATTRIBUTE (ID, USER_ID, ATTR_NAME, ATTR_VALUE) VALUES (104, 3, 'department', 'Accounting'); +INSERT INTO UM_USER_ATTRIBUTE (ID, USER_ID, ATTR_NAME, ATTR_VALUE) VALUES (105, 4, 'department', 'Security'); +INSERT INTO UM_USER_ATTRIBUTE (ID, USER_ID, ATTR_NAME, ATTR_VALUE) VALUES (108, 7, 'department', 'Legal'); + +COMMIT; diff --git a/website/docs/commands/diff.md b/website/docs/commands/diff.md index 79aa382..8745e8f 100644 --- a/website/docs/commands/diff.md +++ b/website/docs/commands/diff.md @@ -28,6 +28,7 @@ Tables with schema drift are skipped for data diffing and flagged in the report. |---|---| | `--config` | Path to the configuration file (default: `deepdiffdb.config.yaml`) | | `--html` | Generate an interactive HTML report (`report.html`) in addition to JSON/text outputs | +| `--row-diff` | Inspect column-level differences for changed rows directly in terminal and write `row_diff.json` and `row_diff.txt` | | `--batch-size N` | Rows per keyset-paginated query (overrides `performance.hash_batch_size`) | | `--parallel N` | Max tables hashed concurrently (overrides `performance.max_parallel_tables`) | | `--verbose` | Enable debug-level logging | @@ -45,6 +46,8 @@ All files are written to `output.dir` (default: `./diff-output`). | `schema_diff.txt` | Human-readable schema diff | | `content_diff.json` | Row-level differences: added, removed, updated rows per table | | `conflicts.json` | Rows that exist in both databases with conflicting values | +| `row_diff.json` | Column-level differences for each changed row (with `--row-diff` or `--html`) | +| `row_diff.txt` | Human-readable side-by-side column diff report (with `--row-diff`) | | `summary.txt` | High-level statistics: tables scanned, rows added/updated/removed, conflict count | | `report.html` | Interactive HTML report (only when `--html` is used) | @@ -54,7 +57,10 @@ All files are written to `output.dir` (default: `./diff-output`). # Basic diff deepdiffdb diff --config deepdiffdb.config.yaml -# With HTML report and streaming flags for large tables +# Full diff with column-level row inspection and interactive HTML report +deepdiffdb diff --config deepdiffdb.config.yaml --row-diff --html + +# With streaming flags for large tables deepdiffdb diff --config deepdiffdb.config.yaml --html --batch-size 5000 --parallel 4 ``` diff --git a/website/docs/commands/index.md b/website/docs/commands/index.md index 58c5930..798e682 100644 --- a/website/docs/commands/index.md +++ b/website/docs/commands/index.md @@ -14,6 +14,7 @@ DeepDiff DB exposes a set of focused commands. Each command does one thing; chai | `schema-diff` | Detect schema drift between production and development | | `schema-migrate` | Generate a standalone schema migration SQL script | | `diff` | Full diff: schema introspection + row-level data comparison | +| `row-diff` | Inspect column-level differences for modified/added/removed rows | | `gen-pack` | Generate a complete SQL migration pack (schema + data) | | `apply` | Apply a migration pack to the production database | | `resolve-conflicts` | Interactively (or automatically) resolve data conflicts | diff --git a/website/docs/commands/row-diff.md b/website/docs/commands/row-diff.md new file mode 100644 index 0000000..f61a919 --- /dev/null +++ b/website/docs/commands/row-diff.md @@ -0,0 +1,116 @@ +--- +sidebar_position: 6 +--- + +# row-diff + +Inspects column-level differences for modified, added, or removed rows between production and development databases. Displays a side-by-side terminal comparison with differing columns highlighted, and writes machine-readable reports. + +![row-diff Terminal Inspection](/img/row-diff-terminal.svg) + +## Usage + +```bash +# Inspect all updated rows across all tables +deepdiffdb row-diff --config deepdiffdb.config.yaml + +# Inspect a specific table and primary key directly +deepdiffdb row-diff --table customers --key 4 + +# Inspect removed rows in JSON format +deepdiffdb row-diff --status removed --format json + +# Run as part of a full diff +deepdiffdb diff --config deepdiffdb.config.yaml --row-diff +``` + +## What It Does + +1. **Hashes & Discovers Differences**: Hashes tables across databases using keyset pagination and identifies modified, removed, or added primary keys. +2. **Cross-Engine Identifier Resolution**: Pairs tables and columns case-insensitively across different database engines (for example, PostgreSQL `customers.email` $\leftrightarrow$ Oracle `CUSTOMERS.EMAIL`). +3. **Selective Row Fetching**: Queries the actual raw column values directly using parameterized queries and dialect-specific identifier quoting. When `--table` and `--key` are supplied, it executes a targeted single-row lookup for instant results. +4. **Side-by-Side Column Comparison**: Compares column values across both engines, flags differences with `*`, and formats NULL and type variations cleanly. +5. **Generates Reports**: Outputs results to stdout and saves `row_diff.json` and `row_diff.txt` in the configured output directory. + +## Flags + +| Flag | Description | Default | +|---|---|---| +| `--config` | Path to configuration file | `deepdiffdb.config.yaml` | +| `--table` | Filter inspection to a specific table name | `""` (all tables) | +| `--key` | Filter inspection to a specific primary key value | `""` (all keys) | +| `--status` | Row status to inspect: `updated`, `added`, `removed`, or `all` | `updated` | +| `--limit` | Maximum differing rows to inspect per table (`0` = unlimited) | `0` | +| `--format` | Output format: `table` (terminal ASCII) or `json` | `table` | +| `--output` | Output directory to save reports | Config output directory | +| `--batch-size N` | Rows per keyset-paginated query | Config default | +| `--parallel N` | Max tables hashed concurrently | Config default | +| `--verbose` | Enable debug logging | `false` | + +## Terminal Output Example + +```text +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Table: customers | Key: 4 (updated) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Column | Production | Development + ---------------------+--------------------------+-------------------------- + country | FR | FR + email | dan@example.com | dan+new@example.com * + id | 4 | 4 + name | Dan | Dan + + * indicates columns that differ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Total differing rows inspected: 1 +Row diff report written to diff-output/row_diff.json and diff-output/row_diff.txt +``` + +## JSON Output Example + +Passing `--format json` outputs structured data ideal for automated CI/CD assertion steps: + +```json +{ + "total_rows": 1, + "tables": [ + { + "table": "customers", + "rows": [ + { + "table": "customers", + "key": "4", + "status": "updated", + "columns": [ + { + "column": "country", + "prod_val": "FR", + "dev_val": "FR", + "differs": false + }, + { + "column": "email", + "prod_val": "dan@example.com", + "dev_val": "dan+new@example.com", + "differs": true + } + ], + "differing_columns": ["email"] + } + ] + } + ] +} +``` + +## Cross-Engine Migrations (PostgreSQL ↔ Oracle / MySQL) + +Different database engines enforce different default identifier casing: +- **PostgreSQL**: folds unquoted identifiers to **lowercase** (`um_user_attribute`) +- **Oracle**: folds unquoted identifiers to **UPPERCASE** (`UM_USER_ATTRIBUTE`) + +`deepdiffdb row-diff` seamlessly bridges this gap: +- Tables are discovered using canonical name folding. +- Each database query preserves the correct engine-specific quoting (`"customers"` in PostgreSQL, `"CUSTOMERS"` in Oracle, `` `customers` `` in MySQL, `[customers]` in MSSQL). +- Column values are paired by canonical name, allowing you to instantly identify which exact columns drifted during an ETL or replication pipeline. diff --git a/website/docs/features/cross-engine-diff.md b/website/docs/features/cross-engine-diff.md new file mode 100644 index 0000000..ac6ed8b --- /dev/null +++ b/website/docs/features/cross-engine-diff.md @@ -0,0 +1,130 @@ +--- +sidebar_position: 1 +--- + +# Cross-Engine Data Diff & Verification + +DeepDiff DB supports **cross-engine data diffing and migration verification**, allowing you to compare schemas and row data between completely different database engines (such as **PostgreSQL $\leftrightarrow$ Oracle**, **MySQL $\leftrightarrow$ PostgreSQL**, or **SQLite $\leftrightarrow$ MSSQL**). + +![Live Migration Pipeline Mismatch](/img/cross-engine-pipeline-mismatch.jpg) + +--- + +## The Cross-Engine Challenge: Identifier Case Folding + +When migrating data between database engines (for example, in an enterprise ETL pipeline migrating from PostgreSQL to Oracle), automated row-count and checksum verification frequently fails or reports false positives: + +```text +TOTAL RECORD COUNTS (entire database) +Source (PG) : 12,923 +Oracle : 19,132 +Delta (Oracle - Source): +6,209 + +MISMATCHED TABLES: +TABLE PG ORACLE DELTA +------------------------------------------------ +UM_USER_ATTRIBUTE 12281 18403 +6122 +UM_USER 640 727 +87 +``` + +### Why Naive Comparisons Fail + +1. **Identifier Folding Discrepancies**: + - **PostgreSQL** folds unquoted identifiers to **lowercase** (`um_user_attribute`, `um_user`). + - **Oracle** folds unquoted identifiers to **UPPERCASE** (`UM_USER_ATTRIBUTE`, `UM_USER`). + - **MySQL** on Linux defaults to case-sensitive table names, while on Windows/macOS it is case-insensitive. + - Traditional tools comparing by exact table name (`"um_user" == "UM_USER"`) find 0 matching tables and report `"No differences found"`, completely missing thousands of drifted rows! + +2. **Column Quoting and Dialect Differences**: + - Querying a PostgreSQL table requires double quotes for mixed-case or lowercase identifiers: `SELECT "id", "email" FROM "customers"`. + - Querying Oracle requires uppercase or quoted identifiers: `SELECT "ID", "EMAIL" FROM "CUSTOMERS"`. + - SQLite and MySQL use backticks or brackets. + +--- + +## How DeepDiff DB Solves This + +DeepDiff DB introduces **engine-aware canonical identifier folding** and **dialect-specific query drivers**: + +```text +┌─────────────────────────┐ ┌─────────────────────────┐ +│ PostgreSQL (Source) │ │ Oracle (Target) │ +│ "um_user" (lowercase) │ │ "UM_USER" (UPPERCASE) │ +└────────────┬────────────┘ └────────────┬────────────┘ + │ │ + ▼ ▼ + Canonical: "um_user" ◄──────────────────► Canonical: "um_user" + (Matched & Paired) + │ + ▼ + ┌─────────────────────────────────────────┐ + │ Keyset-Paginated Row Hashing │ + │ PostgreSQL driver: SELECT "id"... │ + │ Oracle driver: SELECT "ID"... │ + └─────────────────────┬───────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────┐ + │ Side-by-Side Column Diffing │ + │ email: dan@example.com (*) │ + │ email: dan+new@example.com │ + └─────────────────────────────────────────┘ +``` + +1. **Case-Insensitive Table Discovery**: DeepDiff matches tables across engines using case-folded canonical names (`schema.CanonicalIdent`). +2. **Preserved Dialect Execution**: Each database connection uses its own driver and dialect rules, ensuring queries against PostgreSQL use correct lowercase identifiers while queries against Oracle use uppercase identifiers. +3. **Column-Level Value Alignment**: Columns are aligned canonically, so `email` in PostgreSQL is compared directly with `EMAIL` in Oracle regardless of catalog casing. + +--- + +## Inspecting Changed Rows (`row-diff`) + +Once table differences or conflicts are detected, use the `row-diff` command (or pass `--row-diff` to `diff`) to inspect exactly what changed at the column level: + +![row-diff Terminal Output](/img/row-diff-terminal.svg) + +```bash +# Compare PostgreSQL against Oracle with column-level row inspection +deepdiffdb diff --config deepdiffdb.config.yaml --row-diff + +# Or inspect a specific table and primary key directly +deepdiffdb row-diff --table customers --key 4 +``` + +Terminal output highlights differing columns with an asterisk (`*`): + +```text +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Table: customers | Key: 4 (updated) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Column | Production (PostgreSQL) | Development (Oracle) + ---------------------+--------------------------+-------------------------- + country | FR | FR + email | dan@example.com | dan+new@example.com * + id | 4 | 4 + name | Dan | Dan + + * indicates columns that differ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + +## Interactive HTML Report + +Generating an HTML report (`deepdiffdb diff --html`) provides an interactive side-by-side inspection view: + +- **Data Changes Tab**: Opens automatically when data drift exists. Clicking any table displays both key summaries and the complete column comparison table for every modified row. +- **Row Data Tab**: Provides a dedicated card view of every changed row across all tables with real-time table filtering. +- **Conflicts Tab**: Displays conflicting keys with source and target hash values, resolution strategy, and side-by-side column values. + +```bash +deepdiffdb diff --config deepdiffdb.config.yaml --html +open diff-output/report.html +``` + +--- + +## Ready-to-Run Sample + +Check out **[Sample 18: Cross-Engine Row Diff & Verification](https://github.com/iamvirul/deepdiff-db/tree/main/samples/18-cross-engine-row-diff)** in the repository, which sets up PostgreSQL and Oracle containers with pre-seeded data mismatches to test cross-engine verification. diff --git a/website/docs/features/html-reports.md b/website/docs/features/html-reports.md index de6ea8d..8e79e85 100644 --- a/website/docs/features/html-reports.md +++ b/website/docs/features/html-reports.md @@ -56,18 +56,25 @@ Lists every schema object that changed between prod and dev. - Added, removed, and modified sequences - For modified sequences: shows which numeric properties differ (increment, min/max value, cache, cycle) -### Data Diff Tab +### Data Changes Tab - Summary counts per table: rows added, rows removed, rows updated -- Expandable per-table section showing the primary key of each changed row -- Filtering controls to show only added / removed / updated rows +- Click any table to expand and view both primary key listings and full **side-by-side column-level row differences** for every modified, added, or removed row +- Table filter dropdown for quick navigation + +### Row Data Tab + +- Dedicated card view for every differing row across the entire database +- Displays side-by-side columns: **Column name**, **Production value**, and **Development value** +- Asterisks (`*`) and highlighted background colors immediately draw attention to columns that differ +- Filter by table name using the real-time filter dropdown ### Conflicts Tab -- Lists all conflicts grouped by table -- Each conflict shows the full row comparison with changed columns highlighted -- Resolution status badge (auto-resolved, pending, resolved) when `resolutions.json` is present -- Per-table resolution strategy shown when configured +- Lists all conflicts grouped by table with SHA-256 hash comparison (`prod_hash → dev_hash`) +- Embedded column comparison table showing exactly which fields differ +- Resolution status badge (`Keep Source`, `Use Target`, or `Pending`) when resolutions are applied +- Per-table resolution strategy breakdown table ### SQL Migration Preview Tab diff --git a/website/docs/samples/index.md b/website/docs/samples/index.md index b7590db..6a336c8 100644 --- a/website/docs/samples/index.md +++ b/website/docs/samples/index.md @@ -27,6 +27,7 @@ The `samples/` directory in the repository contains 17 self-contained example pr | 15 | [mssql-support](https://github.com/iamvirul/deepdiff-db/tree/main/samples/15-mssql-support) | Full MSSQL workflow: schema diff, data diff, gen-pack, apply | MSSQL | Yes | | 16 | [oracle-support](https://github.com/iamvirul/deepdiff-db/tree/main/samples/16-oracle-support) | Full Oracle workflow: schema drift, data diff, gen-pack, apply | Oracle | Yes | | 17 | [git-like-versioning](https://github.com/iamvirul/deepdiff-db/tree/main/samples/17-git-like-versioning) | Git-like versioning: commit snapshots, branches, checkout, ASCII tree graph, compare versions, generate rollback SQL | MySQL | Yes | +| 18 | [cross-engine-row-diff](https://github.com/iamvirul/deepdiff-db/tree/main/samples/18-cross-engine-row-diff) | Cross-engine migration verification (PostgreSQL ↔ Oracle), case-folding resolution, and column-level `row-diff` | PostgreSQL / Oracle | Yes | ## Running a SQLite Sample @@ -98,3 +99,18 @@ deepdiffdb version diff deepdiffdb version rollback --out diff-output/rollback_v3.sql docker-compose down -v ``` + +## Sample 18: Cross-Engine Row Diff & Migration Verification + +Sample 18 demonstrates real-world cross-engine migration verification between **PostgreSQL 16** (source) and **Oracle XE 21c** (target). It seeds tables with differing default identifier cases (`um_user` in PG vs `UM_USER` in Oracle, and `um_user_attribute` in PG vs `UM_USER_ATTRIBUTE` in Oracle) with intentional column drift: + +```bash +cd samples/18-cross-engine-row-diff +make up # start PostgreSQL and Oracle containers +make wait-healthy # wait for both containers to be ready +make seed # populate schemas and test datasets +make diff # run cross-engine diff +make row-diff # inspect column-level row differences in terminal +make html # generate and open interactive HTML report +make down # stop containers +``` diff --git a/website/sidebars.js b/website/sidebars.js index 9e4c0f2..199929c 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -8,7 +8,7 @@ const sidebars = { }, { type: 'category', label: 'Commands', - items: ['commands/index', 'commands/check', 'commands/schema-diff', 'commands/schema-migrate', 'commands/diff', 'commands/gen-pack', 'commands/apply', 'commands/resolve-conflicts', 'commands/version'], + items: ['commands/index', 'commands/check', 'commands/schema-diff', 'commands/schema-migrate', 'commands/diff', 'commands/row-diff', 'commands/gen-pack', 'commands/apply', 'commands/resolve-conflicts', 'commands/version'], }, { type: 'category', label: 'Database Support', @@ -16,7 +16,7 @@ const sidebars = { }, { type: 'category', label: 'Features', - items: ['features/git-versioning', 'features/streaming', 'features/html-reports', 'features/conflict-resolution', 'features/checkpoint-resume', 'features/logging'], + items: ['features/cross-engine-diff', 'features/git-versioning', 'features/streaming', 'features/html-reports', 'features/conflict-resolution', 'features/checkpoint-resume', 'features/logging'], }, { type: 'category', label: 'Deployment', diff --git a/website/static/img/cross-engine-pipeline-mismatch.jpg b/website/static/img/cross-engine-pipeline-mismatch.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6847139b7aa689e61cef49b53f97bd4492f52b9b GIT binary patch literal 141258 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 mI0KlHwTe`YM6I3 + + + + + + + + + + + + + + + + + + + + + + + + + + deepdiffdb row-diff — Cross-Engine Inspection + + + + + + + $ + deepdiffdb row-diff --table customers --key 4 + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Table: + customers + | + Key: + 4 + + updated + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + Column + | + Production (PostgreSQL) + | + Development (Oracle) + + + --------------------+---------------------------+--------------------------- + + + country + | + FR + | + FR + + + + email + | + dan@example.com + | + dan+new@example.com + * + + + id + | + 4 + | + 4 + + + name + | + Dan + | + Dan + + + * + indicates columns that differ across engines + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + ✓ + 1 row difference inspected (PostgreSQL "customers" ↔ Oracle "CUSTOMERS") + + diff --git a/website/static/samples/report.html b/website/static/samples/report.html index f56d9d3..b7e2a9b 100644 --- a/website/static/samples/report.html +++ b/website/static/samples/report.html @@ -589,6 +589,15 @@ overflow: hidden; } + .conflict-item-card { + border-bottom: 1px solid var(--border); + background: var(--bg); + } + + .conflict-item-card:last-child { + border-bottom: none; + } + .conflict-item { display: grid; grid-template-columns: 140px 1fr 200px 100px; @@ -597,12 +606,78 @@ padding: 12px 16px; background: var(--bg); font-size: 13px; + cursor: pointer; } .conflict-item:hover { background: var(--bg-secondary); } + .conflict-item-body { + padding: 0 16px 14px 16px; + background: var(--bg-secondary); + border-top: 1px dashed var(--border); + } + + .row-data-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + margin-top: 10px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 4px; + overflow: hidden; + } + + .row-data-table th, .row-data-table td { + padding: 7px 12px; + text-align: left; + border-bottom: 1px solid var(--border); + } + + .row-data-table th { + background: var(--bg-tertiary); + font-weight: 600; + color: var(--text-secondary); + } + + .row-data-table tr:last-child td { + border-bottom: none; + } + + .diff-row-changed { + background: rgba(234, 88, 12, 0.08); + } + + .diff-star { + color: #ea580c; + font-weight: bold; + } + + .rowdiff-card { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + margin-bottom: 16px; + overflow: hidden; + } + + .rowdiff-header { + display: flex; + align-items: center; + gap: 16px; + padding: 10px 16px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + font-size: 13px; + font-weight: 500; + } + + .rowdiff-body { + padding: 12px 16px; + } + .conflict-table { font-family: var(--font-mono); font-weight: 500; @@ -774,19 +849,19 @@

Database Diff Report

Generated - 2026-05-03 13:38:34 +0530 + 2026-09-25 08:54:47 +0530
Source - localhost:3316/prod_ecommerce + 127.0.0.1:55432/appdb
Target - localhost:3317/dev_ecommerce + 127.0.0.1:51521/XEPDB1
Version - v1.4.0-dev.20260503T080705Z.59ac4a3 + dev
@@ -797,23 +872,23 @@

Database Diff Report

Schema
-
7
+
1
Tables
-
+20
+
0
Added
-
-7
+
-2
Removed
-
18
+
1
Modified
-
18
+
1
Conflicts
@@ -822,19 +897,24 @@

Database Diff Report

-
+
@@ -842,7 +922,7 @@

Database Diff Report

- feature_flags + CUSTOMERS New Table @@ -855,7 +935,7 @@

Database Diff Report

i
-
Table 'feature_flags' exists in dev but not in prod
+
Table 'CUSTOMERS' exists in dev but not in prod
@@ -868,201 +948,36 @@

Database Diff Report

customers - Modified - -
-
- -
-
~
-
- email - Type changed: varchar(100) -> varchar(255) -
-
- - - - -
-
- -
-
- - - - orders - - Modified - -
-
- - -
-
+
-
- INDEX idx_customer_id - (customer_id) -
-
- -
-
−
-
- INDEX customer_id - (customer_id) -
-
- - - -
-
- -
-
- - - - products - - Modified + Dropped +
-
-
+
-
- discount_percent - Column added: discount_percent decimal(5,2) -
-
- -
-
- - -
-
- - - - Views - 3 change(s) -
-
- -
-
+
-
- v_customer_stats - View 'v_customer_stats' exists in dev but not in prod -
-
- -
-
−
-
- v_customer_summary - View 'v_customer_summary' exists in prod but not in dev -
-
-
-
~
-
- v_active_orders - View definition changed -
+
i
+
Table 'customers' exists in prod but not in dev
-
-
- - - - Routines - 3 change(s) -
-
- -
-
+
-
- FUNCTION: fn_format_price - FUNCTION 'fn_format_price' exists in dev but not in prod -
-
- -
-
~
-
- fn_calculate_total - definition changed; parameters changed -
-
- -
-
~
-
- fn_get_customer_tier - definition changed -
-
- -
-
- -
-
- - - - Triggers - 2 change(s) -
-
- -
-
+
-
- trg_products_updated_at (table: products) - Trigger 'trg_products_updated_at' on table 'products' exists in dev but not in prod -
-
- -
-
~
-
- trg_orders_audit - definition changed -
-
- -
-
-
+
@@ -1077,170 +992,174 @@

Database Diff Report

- - - audit_log - (click to expand) - - — - −7 - — - - - - -
- - -
- Removed Keys: -
89710346
-
- - -
- - - - customers - (click to expand) + (click to view row data) - +1 — - ~5 + −2 + ~1 - +
-
- Added Keys: -
6
-
- - - -
- Modified Keys: -
34512
-
- -
- - - - - - - inventory_log - (click to expand) - - +5 - — - — - - - - -
- -
- Added Keys: -
789106
-
- - - -
- - - - - - - order_items - (click to expand) - - +6 - — - — - - - - -
- -
- Added Keys: -
91113141012
-
- - - -
- - - - - - - orders - (click to expand) - - +5 - — - ~3 - - - - -
- Added Keys: -
106789
+ Removed Keys: +
56
-
Modified Keys: -
453
+
4
-
- - - - - - - products - (click to expand) - - +3 - — - ~10 - - - - -
-
- Added Keys: -
131112
-
- - - -
- Modified Keys: -
13578246910
+
+ Row Data Differences (3 rows): + +
+
+ Key: 4 + updated + Changed: email +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColumnProductionDevelopment
countryFRFR
email *dan@example.comdan+new@example.com
id44
nameDanDan
+
+
+ +
+
+ Key: 5 + removed + Changed: country, email, id, name +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColumnProductionDevelopment
country *INNULL
email *eve@example.comNULL
id *5NULL
name *EveNULL
+
+
+ +
+
+ Key: 6 + removed + Changed: country, email, id, name +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColumnProductionDevelopment
country *JPNULL
email *frank@example.comNULL
id *6NULL
name *FrankNULL
+
+
+
@@ -1263,261 +1182,218 @@

Database Diff Report

-
- orders - 3 -
- 50e98d4f0d2f... - → - 3bc0bb9da318... -
-
- - Pending -
-
- -
- orders - 4 -
- 91c74b904980... - → - 932e3c9169d2... -
-
- - Pending -
-
- -
- orders - 5 -
- 579ea721981c... - → - f71fa3411956... -
-
- - Pending -
-
- -
- products - 10 -
- 056fec0d5c80... - → - 3ebc00ec928f... -
-
- - Pending -
-
- -
- products - 1 -
- 91290ea738a1... - → - 6a0483d92962... -
-
- - Pending -
-
- -
- products - 3 -
- 8d56e3071d26... - → - f0a691c2fd9a... -
-
- - Pending -
-
- -
- products - 5 -
- 809e6e458343... - → - d9e14527579f... -
-
- - Pending -
-
- -
- products - 7 -
- 2c83bd23bb23... - → - 0fceddb13a5d... -
-
- - Pending -
-
- -
- products - 8 -
- 602de4ce149c... - → - 74347a9a8653... -
-
- - Pending -
-
- -
- products - 2 -
- bf9d32823bd2... - → - 0c78ab070b93... -
-
- - Pending -
-
- -
- products - 4 -
- 0a4bb61bbee7... - → - d86101d3905a... -
-
- - Pending -
-
- -
- products - 6 -
- 131705dda71e... - → - fd58eb3590d5... -
-
- - Pending -
-
- -
- products - 9 -
- b58fe1a5cd09... - → - 8fc3ad499b63... -
-
- - Pending -
-
- -
- customers - 2 -
- 99cac2a7c695... - → - 0a949f3c2ddf... +
+
+ customers + 4 +
+ f29abbc96823... + → + e25da4226966... +
+
+ + Pending +
-
- - Pending + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColumnProductionDevelopment
countryFRFR
email *dan@example.comdan+new@example.com
id44
nameDanDan
+
-
- customers - 3 -
- 8f56cdab82c9... - → - 2a3cf69f64dc... -
-
- - Pending -
-
+
+ +
+ + +
+
+ +
+
+ -
- customers - 4 -
- b4257f97cc9f... - → - a16b7ce17528... +
+
+ customers + Key: 4 + updated
-
- - Pending +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColumnProductionDevelopment
countryFRFR
email *dan@example.comdan+new@example.com
id44
nameDanDan
-
- customers - 5 -
- 2014b0374b60... - → - 7f8723118572... +
+
+ customers + Key: 5 + removed
-
- - Pending +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColumnProductionDevelopment
country *INNULL
email *eve@example.comNULL
id *5NULL
name *EveNULL
-
- customers - 1 -
- f30811df6706... - → - dcb1c5d6e44a... +
+
+ customers + Key: 6 + removed
-
- - Pending +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColumnProductionDevelopment
country *JPNULL
email *frank@example.comNULL
id *6NULL
name *FrankNULL
-
- + +
+
@@ -1552,8 +1428,17 @@

Database Diff Report

function filterConflicts() { const v = document.getElementById('conflict-filter').value; - document.querySelectorAll('#conflict-list .conflict-item').forEach(r => { - r.style.display = !v || r.dataset.table === v ? '' : 'none'; + document.querySelectorAll('#conflict-list .conflict-item-card').forEach(r => { + const show = !v || r.dataset.table === v; + r.style.display = show ? '' : 'none'; + }); + } + + function filterRowDiffs() { + const v = document.getElementById('rowdiff-filter').value; + document.querySelectorAll('#rowdiff-list .rowdiff-card').forEach(r => { + const show = !v || r.dataset.table === v; + r.style.display = show ? '' : 'none'; }); } @@ -1583,9 +1468,21 @@

Database Diff Report

const cf = document.getElementById('conflict-filter'); if (cf) { const ts = new Set(); - document.querySelectorAll('#conflict-list .conflict-item').forEach(i => ts.add(i.dataset.table)); + document.querySelectorAll('#conflict-list .conflict-item-card').forEach(i => ts.add(i.dataset.table)); ts.forEach(t => { const o = document.createElement('option'); o.value = t; o.textContent = t; cf.appendChild(o); }); } + + const rf = document.getElementById('rowdiff-filter'); + if (rf) { + const rts = new Set(); + document.querySelectorAll('#rowdiff-list .rowdiff-card').forEach(i => rts.add(i.dataset.table)); + rts.forEach(t => { const o = document.createElement('option'); o.value = t; o.textContent = t; rf.appendChild(o); }); + } + + const hash = window.location.hash.replace('#', ''); + if (hash && document.getElementById('tab-' + hash)) { + switchTab(hash); + } }); From be5a732966207fa73740d89029bf6b775766f1ae Mon Sep 17 00:00:00 2001 From: iamvirul Date: Fri, 25 Sep 2026 09:07:20 +0530 Subject: [PATCH 4/7] docs: use newly generated cross-engine HTML report as documentation sample --- website/docs/features/cross-engine-diff.md | 2 ++ website/docs/features/html-reports.md | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/website/docs/features/cross-engine-diff.md b/website/docs/features/cross-engine-diff.md index ac6ed8b..e86192d 100644 --- a/website/docs/features/cross-engine-diff.md +++ b/website/docs/features/cross-engine-diff.md @@ -112,6 +112,8 @@ Table: customers | Key: 4 (updated) ## Interactive HTML Report +**[→ View Live Interactive Report](pathname:///samples/report.html)** + Generating an HTML report (`deepdiffdb diff --html`) provides an interactive side-by-side inspection view: - **Data Changes Tab**: Opens automatically when data drift exists. Clicking any table displays both key summaries and the complete column comparison table for every modified row. diff --git a/website/docs/features/html-reports.md b/website/docs/features/html-reports.md index 8e79e85..270a1ed 100644 --- a/website/docs/features/html-reports.md +++ b/website/docs/features/html-reports.md @@ -10,7 +10,11 @@ DeepDiff DB can generate a self-contained, interactive HTML report alongside the **[→ View Sample Report](pathname:///samples/report.html)** -The sample report is generated from two real MySQL databases with intentional schema drift: added/removed/modified views, routines, and triggers, plus table-level column and index changes with data conflicts. +The sample report is generated directly from a cross-engine migration diff (**PostgreSQL $\leftrightarrow$ Oracle XE**). It demonstrates: +- The **Data Changes** tab with automatic expansion and embedded column-level comparisons (`*` highlighting differing fields like `email`) +- The dedicated **Row Data** tab with per-table filtering across all differing rows +- The **Conflicts** tab with SHA-256 hash comparison and column difference tables +- Full print and PDF export styling ## Generating a Report From 8f400421b470de06711d7868886b611513a66cb9 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Fri, 25 Sep 2026 09:08:46 +0530 Subject: [PATCH 5/7] docs: update CHANGELOG.md for cross-engine diff and row-diff features --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bccc9a4..7a0f197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Cross-engine case-insensitive table discovery & row hashing**: + - Tables are discovered and paired across database engines that enforce different default identifier cases (e.g., PostgreSQL lowercase `"customers"` $\leftrightarrow$ Oracle UPPERCASE `"CUSTOMERS"`). + - Normalizes identifiers via canonical name folding (`schema.CanonicalIdent`) while generating dialect-specific SQL quoting for each engine. +- **Column-level row inspection (`row-diff` command and `--row-diff` flag)**: + - New CLI command `deepdiffdb row-diff` and `--row-diff` flag for `deepdiffdb diff`. + - Side-by-side terminal comparison table showing column values between production and development with differing columns flagged with `*`. + - Supports `--table`, `--key`, `--status` (`updated`, `added`, `removed`, `all`), `--limit`, and `--format` (`table` or `json`). + - Writes machine-readable `row_diff.json` and formatted text `row_diff.txt` to the output directory. +- **HTML report viewer enhancements**: + - **Data Changes Tab**: Opens automatically when data drift exists; expanding any table displays the complete column comparison table for every modified, added, or removed row. + - **Dedicated Row Data Tab**: Card view of every differing row across all tables with real-time table filtering and diff counters. + - **Conflicts Tab**: Displays conflicting keys with source and target SHA-256 hashes alongside side-by-side column difference tables. +- **Documentation & Sample Project**: + - New feature guide: `website/docs/features/cross-engine-diff.md` explaining cross-engine identifier folding and pipeline verification. + - New command guide: `website/docs/commands/row-diff.md`. + - Live interactive sample HTML report updated at `/samples/report.html`. + - New **Sample 18** (`samples/18-cross-engine-row-diff/`): PostgreSQL 16 $\leftrightarrow$ Oracle XE 21c migration verification with seeded `um_user` and `um_user_attribute` drift. + +### Fixed + +- **False negative "No differences found" on cross-engine migrations**: + - Fixed an issue where `deepdiffdb diff` reported zero differences between PostgreSQL and Oracle because exact string matching failed on uppercase vs. lowercase table names (e.g. `um_user_attribute` vs. `UM_USER_ATTRIBUTE`). + ## [1.4.5] - 2026-05-19 ### Security From 3d17a85cc0f99881005d8811ca46c83a4c0c18a6 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Fri, 25 Sep 2026 09:46:14 +0530 Subject: [PATCH 6/7] fix(security): resolve govulncheck vulnerabilities by updating Go to 1.25.13 and text to v0.39.0 --- .github/workflows/ci.yml | 16 ++++++++-------- .github/workflows/security.yml | 2 +- CHANGELOG.md | 16 ++++++++++++++++ go.mod | 6 +++--- go.sum | 16 ++++++++-------- 5 files changed, 36 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c64e41..ad05127 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.24', '1.25.10'] + go-version: ['1.24', '1.25.13'] steps: - name: Checkout code @@ -44,7 +44,7 @@ jobs: continue-on-error: false - name: Merge coverage files - if: matrix.go-version == '1.25.10' + if: matrix.go-version == '1.25.13' run: | # Merge coverage files using go tool cover echo "mode: atomic" > coverage.txt @@ -52,7 +52,7 @@ jobs: continue-on-error: false - name: Verify coverage file exists - if: matrix.go-version == '1.25.10' + if: matrix.go-version == '1.25.13' run: | if [ ! -f coverage.txt ]; then echo "Error: coverage.txt not found" @@ -62,7 +62,7 @@ jobs: head -5 coverage.txt - name: Upload coverage artifact for merging - if: matrix.go-version == '1.25.10' + if: matrix.go-version == '1.25.13' uses: actions/upload-artifact@v4 with: name: coverage-unit-${{ github.run_id }} @@ -81,7 +81,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.25.10' + go-version: '1.25.13' cache-dependency-path: go.sum - name: Verify dependencies @@ -133,7 +133,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.25.10' + go-version: '1.25.13' cache-dependency-path: go.sum - name: Download unit test coverage artifact @@ -176,7 +176,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.25.10' + go-version: '1.25.13' cache-dependency-path: go.sum - name: Run linter @@ -198,7 +198,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.25.10' + go-version: '1.25.13' cache-dependency-path: go.sum - name: Download dependencies diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 69d4a98..d860717 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.25.9' + go-version-file: go.mod cache-dependency-path: go.sum - name: Run gosec diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a0f197..1d89d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **False negative "No differences found" on cross-engine migrations**: - Fixed an issue where `deepdiffdb diff` reported zero differences between PostgreSQL and Oracle because exact string matching failed on uppercase vs. lowercase table names (e.g. `um_user_attribute` vs. `UM_USER_ATTRIBUTE`). +### Security + +- **Bumped Go to 1.25.13**: Resolves Go standard library vulnerabilities reported by `govulncheck`: + - **GO-2026-6218**: Quadratic complexity in `net/url` + - **GO-2026-6091**: Context tracking in `html/template` + - **GO-2026-6090** & **GO-2026-5856**: Handshake and Encrypted Client Hello handling in `crypto/tls` + - **GO-2026-6088**: Recursion depth guard in `encoding/xml` + - **GO-2026-5972**: Maximum recursion depth in `encoding/asn1` + - **GO-2026-5039**: Input formatting in `net/textproto` + - **GO-2026-5037**: Inefficient hostname parsing in `crypto/x509` + - **GO-2026-5026**: Punycode label validation in `net/http` +- **Bumped `golang.org/x/text` to v0.39.0**: + - **GO-2026-5970**: Infinite loop on invalid input in `golang.org/x/text` +- **Updated `security.yml` workflow**: + - Switched from hardcoded older Go version (`1.25.9`) to `go-version-file: go.mod` to ensure CI security scans always run with the active patched toolchain. + ## [1.4.5] - 2026-05-19 ### Security diff --git a/go.mod b/go.mod index 664c904..ec9913c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/iamvirul/deepdiff-db -go 1.25.10 +go 1.25.13 require ( github.com/go-sql-driver/mysql v1.9.3 @@ -12,7 +12,7 @@ require ( github.com/testcontainers/testcontainers-go/modules/mssql v0.41.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 - golang.org/x/sync v0.20.0 + golang.org/x/sync v0.21.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.40.1 ) @@ -85,7 +85,7 @@ require ( golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/text v0.39.0 // indirect google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect modernc.org/libc v1.66.10 // indirect diff --git a/go.sum b/go.sum index 1c4b2af..c748cf8 100644 --- a/go.sum +++ b/go.sum @@ -204,12 +204,12 @@ golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -219,12 +219,12 @@ golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= From a938c78e280c5c2100507d704f3fb274a9b531e3 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Fri, 25 Sep 2026 09:55:59 +0530 Subject: [PATCH 7/7] fix(lint): remove unused buildConflictItemsWithResolutions function --- internal/report/html/generator.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/report/html/generator.go b/internal/report/html/generator.go index e2bfd92..f648247 100644 --- a/internal/report/html/generator.go +++ b/internal/report/html/generator.go @@ -468,10 +468,6 @@ func buildTableDiffs(dataDiff *content.DataDiff, opts *ReportOptions) []TableDif return diffs } -// buildConflictItemsWithResolutions converts conflicts to display format with resolution details. -func buildConflictItemsWithResolutions(conflicts *content.Conflicts, resolutions []resolve.Resolution) []ConflictDisplay { - return buildConflictItemsWithResolutionsAndRowDiffs(conflicts, resolutions, nil) -} // buildConflictItemsWithResolutionsAndRowDiffs converts conflicts to display format with resolution and row diff details. func buildConflictItemsWithResolutionsAndRowDiffs(conflicts *content.Conflicts, resolutions []resolve.Resolution, rowReport *resolve.RowDiffReport) []ConflictDisplay {