How drawDB's Deep Diff Algorithm Detects Schema Changes

The deep diff algorithm in drawDB compares two DBML diagram objects by recursively diffing tables, fields, and relationships to produce a structured change report.

drawDB uses a custom deep diff implementation to detect schema modifications between diagram versions. Located in src/utils/dbml/diff.js, this algorithm powers migration generation and change visualization throughout the application. Understanding how it works helps developers extend drawDB or integrate its diffing logic into their own database tooling.

Core Diff Architecture

The algorithm follows a three-layer recursive structure. Each layer transforms complex objects into comparable maps, then classifies items as added, removed, or updated.

Table-Level Diffing

The diffTables function (lines 84-106) handles the first layer of comparison. It builds name-keyed maps for both diagram states, then categorizes each table:

  • Added: Present in "after" but not "before"
  • Removed: Present in "before" but not "after"
  • Existing: Present in both, triggering field-level diffing

This map-based approach provides O(n) complexity for table detection rather than nested iteration.

// Conceptual flow from src/utils/dbml/diff.js
function diffTables(beforeTables, afterTables) {
  const beforeMap = Object.fromEntries(beforeTables.map(t => [t.name, t]));
  const afterMap = Object.fromEntries(afterTables.map(t => [t.name, t]));
  
  // Classification logic yields added/removed/updated arrays
}

Field-Level Comparison

For tables that exist in both states, diffFields (lines 58-78) performs granular column analysis. The function compares field properties across eight specific attributes:

Property Comparison Behavior
type Data type changes (e.g., VARCHAR → TEXT)
enum Enumeration value modifications
default Default value additions or changes
note Documentation/description updates
unique Constraint toggles
increment Auto-increment flag changes
nullability Nullable ↔ NOT NULL transitions
pk Primary key designation

A field is marked updated only when at least one property differs. This prevents false positives from object reference changes or property ordering differences.

// Example: Detecting a modified column
const fieldDiff = diffFields(oldTable.fields, newTable.fields);
// Returns: { added: [], removed: [], updated: [{ name: 'email', changes: ['unique'] }] }

Relationship Diffing

The diffRelationships function (lines 116-148) handles foreign key changes. Relationships are matched using a composite key combining:

  • Source table name
  • Target table name
  • Source field name(s)
  • Target field name(s)

This composite matching prevents collisions when multiple relationships exist between the same tables.

The Top-Level diffDiagram Function

All three diff layers converge in diffDiagram, which returns a standardized structure:

{
  tables: {
    added: [...],      // New table definitions
    removed: [...],   // Dropped tables
    updated: [...]    // Tables with field changes
  },
  relationships: {
    added: [...],
    removed: [...],
    updated: [...]
  }
}

This plain object format enables downstream consumption by:

Deterministic Output Guarantees

The algorithm enforces consistent diff ordering through alphabetic sorting at each comparison layer. This determinism ensures that:

  • Generated migrations are reproducible
  • UI change lists render predictably
  • Diff objects can be safely serialized and hashed

Missing properties are treated as explicit changes rather than ignored, preventing silent drift when metadata is partially defined.

Practical Usage Examples

Generating a Schema Diff

import { diffDiagram } from '@/utils/dbml/diff';

const schemaDiff = diffDiagram(previousDiagram, currentDiagram);

// Inspect table additions
console.log('New tables:', schemaDiff.tables.added.map(t => t.name));

Converting Diff to SQL

import { diffToSQL } from '@/utils/migrations/diffToSQL';

const statements = diffToSQL(schemaDiff);
// Produces: ALTER TABLE ... ADD COLUMN ..., DROP COLUMN ..., etc.

Applying Diff to UI State

// Within a React component or state manager
diagram.applyDiff(schemaDiff);
// Updates visual indicators: green (added), red (removed), yellow (modified)

Summary

  • Map-based comparison at tables, fields, and relationships layers provides linear time complexity
  • Eight property checks per field ensure granular change detection without false positives
  • Composite key matching for relationships handles complex multi-column foreign keys
  • Deterministic alphabetic sorting guarantees reproducible output across runs
  • Plain object return structure enables flexible downstream consumption by migrations, UI, and exports

Frequently Asked Questions

How does drawDB handle renamed tables or columns?

The deep diff algorithm treats renames as remove + add operations rather than updates. This conservative approach ensures data safety—migrations explicitly drop the old structure and create the new one, requiring user confirmation for destructive changes.

What happens when field properties are undefined in one version?

Undefined values are compared as explicit states. If a field has default: undefined in the before state and default: 'none' in the after state, this registers as an update. This prevents silent acceptance of missing metadata that could affect database behavior.

Can the diff algorithm detect index changes?

The current implementation in src/utils/dbml/diff.js focuses on tables, fields, and relationships. Index modifications are handled separately in the DBML parsing layer—indexes are treated as table properties and would appear in field-level updates when their definitions change.

Where does drawDB use the diff output in the application?

The primary consumer is src/utils/migrations/diffToSQL.js, which transforms diff objects into executable ALTER TABLE statements. The editor UI (src/pages/Editor.jsx and src/hooks/useDiagram.js) also uses diff results to highlight modified elements with color-coded borders and change badges.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →