Skip to content

feat(diff): case-insensitive table matching and row hashing for cross-engine diffs - #127

Merged
iamvirul merged 1 commit into
mainfrom
feat/cross-engine-case-insensitive-data-diff
Sep 25, 2026
Merged

iamvirul merged 1 commit into
mainfrom
feat/cross-engine-case-insensitive-data-diff

Conversation

@iamvirul

@iamvirul iamvirul commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Problem

Cross-engine data diffs (e.g. PostgreSQL → Oracle) silently reported "No data differences found", even when the target was missing rows or had drifted values.

Root cause: engines fold unquoted identifier case differently — PostgreSQL stores customers, Oracle stores CUSTOMERS. Table matching and the row hash keyed on the raw identifier, so:

  • no tables matched (customers ≠ CUSTOMERS)
  • zero rows were hashed/compared
  • real missing and changed rows went undetected

For a migration-verification tool, that's a dangerous false negative.

Fix

Compare on a canonical (case-folded) identifier, while preserving the real identifier for SQL generation — quoted identifiers are case-sensitive in most engines (notably Oracle), so lower-casing a real Oracle table name would break the query.

  • schema.CanonicalIdent — case/whitespace-normalized identifier for matching
  • content.orderedColumns — order columns by canonical name so both engines produce a comparable sequence
  • content.hashRow — emit canonical column names so identical row data hashes equal regardless of case
  • content.BuildDataDiff — match tables by canonical name; look up hashes by each side's real name; label output with the prod-side name
  • runDiff — build the shared-table set by canonical name so both sides get hashed

Scope / non-goals

  • Schema drift is still reported honestly. Legitimate cross-engine type differences (integer vs number, varchar vs varchar2) remain visible — they are real differences, not masked.
  • gen-pack / apply SQL generation is unchanged. Generating a cross-engine migration pack to backfill the target is a separate, larger change (type mapping, quoting) and is intentionally out of scope here.

Verification

End-to-end against live PostgreSQL 16 + Oracle XE 21 containers (Postgres = source of truth with 6 rows, Oracle = migrated target with 4 rows, one row's value changed):

Before → No data differences found (count=0 tables matched)
After →

{ "table": "customers", "removed_keys": ["5","6"], "updated_keys": ["4"] }

i.e. ids 5 & 6 missing in Oracle, id 4 drifted — correct.

Tests

  • schema.TestCanonicalIdent (+ cross-engine match invariant)
  • content.TestHashTable_CrossEngineCaseInsensitive — identical data hashes equal across identifier case
  • content.TestBuildDataDiff_CaseInsensitiveTableMatch — customers ↔ CUSTOMERS matches; missing/changed rows detected

All existing content/schema tests continue to pass.

Summary by CodeRabbit

  • Bug Fixes
    • Table and column names that differ only by capitalization or surrounding whitespace are now matched consistently across database engines.
    • Data comparisons produce consistent hashes despite differences in identifier casing, with stable diff ordering and production-side table labels.

Cross-engine data diffs (e.g. PostgreSQL -> Oracle) silently reported
"No data differences found" because identifier case folding differs
between engines: PostgreSQL stores unquoted names lower-case ("customers")
while Oracle stores them upper-case ("CUSTOMERS"). Table matching and the
row hash keyed on the raw identifier, so no tables matched, zero rows were
compared, and real missing/changed rows went undetected — a dangerous
false negative for a migration-verification tool.

Fix the data-diff path to compare on a canonical (case-folded) identifier
while preserving the real identifier for SQL generation, since quoted
identifiers are case-sensitive in most engines (notably Oracle):

- schema.CanonicalIdent: case/whitespace-normalized identifier for matching
- content.orderedColumns: order columns by canonical name so both engines
  produce a comparable column sequence
- content.hashRow: emit canonical column names so identical row data hashes
  equal regardless of identifier case
- content.BuildDataDiff: match tables by canonical name, look up hashes by
  each side's real name, label output with the prod-side name
- runDiff: build the shared-table set by canonical name so both sides hash

Schema drift (legitimate cross-engine type differences such as integer vs
number, varchar vs varchar2) is still reported honestly and unchanged.
gen-pack/apply SQL generation is unaffected and remains a follow-up for
cross-engine migration packs.

Verified end-to-end against live PostgreSQL and Oracle XE containers:
the diff now reports rows missing in Oracle (removed_keys) and value
drift (updated_keys) correctly.

Tests:
- schema.TestCanonicalIdent (+ cross-engine match invariant)
- content.TestHashTable_CrossEngineCaseInsensitive
- content.TestBuildDataDiff_CaseInsensitiveTableMatch
@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: f03bf54b-96ca-4ccb-be02-50e5487dc910

📥 Commits

Reviewing files that changed from the base of the PR and between 9c8b349 and 4100c6a.

📒 Files selected for processing (7)
  • cmd/deepdiffdb/main.go
  • internal/content/diff.go
  • internal/content/hash.go
  • internal/schema/identifiers.go
  • internal/schema/identifiers_test.go
  • tests/content/diff_test.go
  • tests/content/hash_test.go
 ___________________________________
< Colonel Panic is my arch-nemesis. >
 -----------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@iamvirul
iamvirul merged commit ae4b79b into main Sep 25, 2026
14 of 16 checks passed
@codecov

codecov Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.73684% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/content/diff.go 92.30% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant