Skip to content

Schema Inspector ​

The Schema Inspector allows you to examine existing database schemas and reverse engineer them into migration files. This is incredibly useful for:

  • Documenting existing databases
  • Generating migrations from legacy systems
  • Comparing schemas across environments
  • Understanding database structure programmatically

Introduction ​

The Inspector class provides methods to introspect your database and extract detailed information about tables, columns, indexes, and foreign keys.

Getting Started ​

Basic Setup ​

php
use Bob\Database\Connection;
use Bob\Schema\Inspector;

$connection = new Connection([
    'driver' => 'mysql',
    'host' => 'localhost',
    'database' => 'myapp',
    'username' => 'root',
    'password' => 'password',
]);

$inspector = new Inspector($connection);

Inspecting Tables ​

Get All Tables ​

php
$tables = $inspector->getTables();

// Returns array of table names
// ['users', 'posts', 'comments', 'categories']

Database-Specific Behavior ​

The method works across all supported databases:

  • MySQL: SHOW TABLES
  • PostgreSQL: Queries pg_catalog.pg_tables
  • SQLite: Queries sqlite_master

Inspecting Columns ​

Get Table Columns ​

php
$columns = $inspector->getColumns('users');

// Returns detailed column information
print_r($columns);

Example output:

php
[
    [
        'name' => 'id',
        'type' => 'bigint unsigned',
        'nullable' => false,
        'default' => null,
        'auto_increment' => true,
        'primary' => true,
        'unique' => false,
        'length' => null,
        'precision' => 20,
        'scale' => 0,
        'comment' => 'Primary key',
    ],
    [
        'name' => 'name',
        'type' => 'varchar(255)',
        'nullable' => false,
        'default' => null,
        'auto_increment' => false,
        'primary' => false,
        'unique' => false,
        'length' => 255,
        'precision' => null,
        'scale' => null,
        'comment' => '',
    ],
    [
        'name' => 'email',
        'type' => 'varchar(255)',
        'nullable' => false,
        'default' => null,
        'auto_increment' => false,
        'primary' => false,
        'unique' => true,
        'length' => 255,
        'precision' => null,
        'scale' => null,
        'comment' => 'User email address',
    ],
    // ... more columns
]

Column Information Fields ​

Each column array contains:

  • name - Column name
  • type - Database-specific type (e.g., "varchar(255)", "bigint unsigned")
  • nullable - Whether NULL values are allowed (boolean)
  • default - Default value (mixed or null)
  • auto_increment - Whether column auto-increments (boolean, MySQL only)
  • primary - Whether column is primary key (boolean)
  • unique - Whether column has unique constraint (boolean)
  • length - For string types (int or null)
  • precision - For numeric types (int or null)
  • scale - For decimal types (int or null)
  • comment - Column comment (string, MySQL only)

Inspecting Indexes ​

Get Table Indexes ​

php
$indexes = $inspector->getIndexes('users');

print_r($indexes);

Example output:

php
[
    [
        'name' => 'PRIMARY',
        'columns' => ['id'],
        'unique' => true,
        'primary' => true,
    ],
    [
        'name' => 'users_email_unique',
        'columns' => ['email'],
        'unique' => true,
        'primary' => false,
    ],
    [
        'name' => 'users_status_index',
        'columns' => ['status'],
        'unique' => false,
        'primary' => false,
    ],
    [
        'name' => 'users_created_at_index',
        'columns' => ['created_at', 'updated_at'],
        'unique' => false,
        'primary' => false,
    ],
]

Index Information Fields ​

  • name - Index name
  • columns - Array of column names in the index
  • unique - Whether index enforces uniqueness (boolean)
  • primary - Whether this is the primary key (boolean)

Inspecting Foreign Keys ​

Get Foreign Key Constraints ​

php
$foreignKeys = $inspector->getForeignKeys('posts');

print_r($foreignKeys);

Example output:

php
[
    [
        'name' => 'posts_user_id_foreign',
        'column' => 'user_id',
        'foreign_table' => 'users',
        'foreign_column' => 'id',
        'on_update' => 'cascade',
        'on_delete' => 'cascade',
    ],
    [
        'name' => 'posts_category_id_foreign',
        'column' => 'category_id',
        'foreign_table' => 'categories',
        'foreign_column' => 'id',
        'on_update' => 'cascade',
        'on_delete' => 'set null',
    ],
]

Foreign Key Information Fields ​

  • name - Constraint name
  • column - Local column name
  • foreign_table - Referenced table
  • foreign_column - Referenced column
  • on_update - Action on update: "cascade", "restrict", "set null", "no action"
  • on_delete - Action on delete: "cascade", "restrict", "set null", "no action"

Generating Migrations ​

Auto-Generate Migration Files ​

The most powerful feature of the Inspector is automatic migration generation:

php
$migration = $inspector->generateMigration('users');

// Save to file
$filename = '2024_01_15_120000_create_users_table.php';
file_put_contents($filename, $migration);

Example generated migration:

php
<?php

use Bob\Database\Migrations\Migration;
use Bob\Schema\Blueprint;
use Bob\Schema\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id('id');
            $table->string('name', 255);
            $table->string('email', 255)->unique();
            $table->string('password', 255);
            $table->rememberToken();
            $table->dateTime('email_verified_at')->nullable();
            $table->dateTime('created_at')->default('CURRENT_TIMESTAMP');
            $table->dateTime('updated_at')->default('CURRENT_TIMESTAMP');
            $table->index(['created_at']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};

Type Mapping ​

The Inspector intelligently maps database types to Blueprint methods:

Database TypeBlueprint Method
BIGINTbigInteger()
INTinteger()
SMALLINTsmallInteger()
TINYINTtinyInteger()
VARCHARstring()
CHARchar()
TEXTtext()
LONGTEXTlongText()
MEDIUMTEXTmediumText()
DECIMALdecimal()
FLOATfloat()
DOUBLEdouble()
BOOLEANboolean()
DATEdate()
DATETIMEdateTime()
TIMESTAMPtimestamp()
TIMEtime()
JSONjson()
BLOBbinary()
UUIDuuid()

Auto-increment columns are automatically converted to id() or increments().

Complete Examples ​

Documenting an Entire Database ​

php
use Bob\Schema\Inspector;

$inspector = new Inspector($connection);

// Get all tables
$tables = $inspector->getTables();

$documentation = [];

foreach ($tables as $table) {
    $documentation[$table] = [
        'columns' => $inspector->getColumns($table),
        'indexes' => $inspector->getIndexes($table),
        'foreign_keys' => $inspector->getForeignKeys($table),
    ];
}

// Save as JSON
file_put_contents('database_schema.json', json_encode($documentation, JSON_PRETTY_PRINT));

Generating Migrations for All Tables ​

php
$inspector = new Inspector($connection);
$tables = $inspector->getTables();

$timestamp = time();
$migrationDir = __DIR__ . '/migrations';

if (!is_dir($migrationDir)) {
    mkdir($migrationDir, 0755, true);
}

foreach ($tables as $index => $table) {
    // Generate migration content
    $migration = $inspector->generateMigration($table);

    // Create timestamp (increment by 1 second for each migration)
    $time = $timestamp + $index;
    $date = date('Y_m_d_His', $time);

    // Create filename
    $filename = "{$date}_create_{$table}_table.php";
    $filepath = $migrationDir . '/' . $filename;

    // Save migration file
    file_put_contents($filepath, $migration);

    echo "Generated migration for {$table}: {$filename}\n";
}

Comparing Schemas Across Environments ​

php
function compareSchemas(Connection $conn1, Connection $conn2): array
{
    $inspector1 = new Inspector($conn1);
    $inspector2 = new Inspector($conn2);

    $tables1 = $inspector1->getTables();
    $tables2 = $inspector2->getTables();

    $differences = [
        'missing_in_env2' => array_diff($tables1, $tables2),
        'missing_in_env1' => array_diff($tables2, $tables1),
        'column_differences' => [],
    ];

    // Compare columns for tables that exist in both
    $commonTables = array_intersect($tables1, $tables2);

    foreach ($commonTables as $table) {
        $columns1 = $inspector1->getColumns($table);
        $columns2 = $inspector2->getColumns($table);

        $columnNames1 = array_column($columns1, 'name');
        $columnNames2 = array_column($columns2, 'name');

        if ($columnNames1 !== $columnNames2) {
            $differences['column_differences'][$table] = [
                'env1' => $columnNames1,
                'env2' => $columnNames2,
            ];
        }
    }

    return $differences;
}

// Usage
$dev = new Connection([/* dev config */]);
$prod = new Connection([/* prod config */]);

$diff = compareSchemas($dev, $prod);
print_r($diff);

WordPress Database Documentation ​

php
use Bob\Schema\Inspector;

function documentWordPressDatabase(): void
{
    global $wpdb;

    $connection = new Connection([
        'driver' => 'mysql',
        'host' => DB_HOST,
        'database' => DB_NAME,
        'username' => DB_USER,
        'password' => DB_PASSWORD,
        'prefix' => $wpdb->prefix,
    ]);

    $inspector = new Inspector($connection);
    $tables = $inspector->getTables();

    echo "# WordPress Database Schema\n\n";

    foreach ($tables as $table) {
        echo "## Table: {$table}\n\n";

        // Columns
        $columns = $inspector->getColumns($table);
        echo "### Columns\n\n";
        echo "| Name | Type | Nullable | Default | Comment |\n";
        echo "|------|------|----------|---------|----------|\n";

        foreach ($columns as $column) {
            $nullable = $column['nullable'] ? 'YES' : 'NO';
            $default = $column['default'] ?? 'NULL';
            $comment = $column['comment'] ?? '';

            echo "| {$column['name']} | {$column['type']} | {$nullable} | {$default} | {$comment} |\n";
        }

        // Indexes
        $indexes = $inspector->getIndexes($table);
        if (!empty($indexes)) {
            echo "\n### Indexes\n\n";
            echo "| Name | Columns | Type |\n";
            echo "|------|---------|------|\n";

            foreach ($indexes as $index) {
                $columns = implode(', ', $index['columns']);
                $type = $index['primary'] ? 'PRIMARY' : ($index['unique'] ? 'UNIQUE' : 'INDEX');
                echo "| {$index['name']} | {$columns} | {$type} |\n";
            }
        }

        // Foreign Keys
        $foreignKeys = $inspector->getForeignKeys($table);
        if (!empty($foreignKeys)) {
            echo "\n### Foreign Keys\n\n";
            echo "| Name | Column | References | On Update | On Delete |\n";
            echo "|------|--------|------------|-----------|------------|\n";

            foreach ($foreignKeys as $fk) {
                $ref = "{$fk['foreign_table']}.{$fk['foreign_column']}";
                echo "| {$fk['name']} | {$fk['column']} | {$ref} | {$fk['on_update']} | {$fk['on_delete']} |\n";
            }
        }

        echo "\n---\n\n";
    }
}

Use Cases ​

1. Legacy Database Migration ​

When taking over a legacy project without migrations:

php
// Generate migrations for entire legacy database
$inspector = new Inspector($legacyConnection);
$tables = $inspector->getTables();

foreach ($tables as $table) {
    $migration = $inspector->generateMigration($table);
    // Save migration file...
}

2. Schema Validation ​

Verify production schema matches expected structure:

php
$inspector = new Inspector($connection);

// Check if table exists
if (!in_array('users', $inspector->getTables())) {
    throw new Exception('Users table is missing!');
}

// Check for required columns
$columns = array_column($inspector->getColumns('users'), 'name');
$required = ['id', 'email', 'password', 'created_at', 'updated_at'];

$missing = array_diff($required, $columns);
if (!empty($missing)) {
    throw new Exception('Missing columns: ' . implode(', ', $missing));
}

3. Database Documentation ​

Generate markdown documentation:

php
function generateMarkdownDocs(Inspector $inspector, string $table): string
{
    $md = "# Table: {$table}\n\n";

    // Columns
    $columns = $inspector->getColumns($table);
    $md .= "## Columns\n\n";
    $md .= "| Column | Type | Nullable | Default | Comment |\n";
    $md .= "|--------|------|----------|---------|----------|\n";

    foreach ($columns as $col) {
        $nullable = $col['nullable'] ? '✓' : '✗';
        $default = $col['default'] ?? 'NULL';
        $comment = $col['comment'] ?? '';
        $md .= "| {$col['name']} | {$col['type']} | {$nullable} | {$default} | {$comment} |\n";
    }

    return $md;
}

4. Schema Sync Tool ​

Build a tool to sync development to production schema:

php
class SchemaSyncer
{
    public function __construct(
        private Inspector $sourceInspector,
        private Inspector $targetInspector
    ) {}

    public function sync(): array
    {
        $operations = [];

        $sourceTables = $this->sourceInspector->getTables();
        $targetTables = $this->targetInspector->getTables();

        // Find missing tables
        $missingTables = array_diff($sourceTables, $targetTables);

        foreach ($missingTables as $table) {
            $operations[] = [
                'type' => 'create_table',
                'table' => $table,
                'migration' => $this->sourceInspector->generateMigration($table),
            ];
        }

        // Compare columns for existing tables
        $commonTables = array_intersect($sourceTables, $targetTables);

        foreach ($commonTables as $table) {
            $diff = $this->compareColumns($table);
            if (!empty($diff)) {
                $operations[] = [
                    'type' => 'modify_table',
                    'table' => $table,
                    'changes' => $diff,
                ];
            }
        }

        return $operations;
    }

    private function compareColumns(string $table): array
    {
        $sourceColumns = $this->sourceInspector->getColumns($table);
        $targetColumns = $this->targetInspector->getColumns($table);

        $sourceNames = array_column($sourceColumns, 'name');
        $targetNames = array_column($targetColumns, 'name');

        return [
            'missing' => array_diff($sourceNames, $targetNames),
            'extra' => array_diff($targetNames, $sourceNames),
        ];
    }
}

Best Practices ​

1. Always Review Generated Migrations ​

Generated migrations are a starting point - always review and adjust:

php
$migration = $inspector->generateMigration('users');

// Save for review
file_put_contents('migration_draft.php', $migration);

// Review, then move to migrations directory

2. Handle Complex Types ​

Some database-specific types may need manual adjustment:

php
// Generated might use:
$table->string('data');

// But you might want:
$table->json('data');

3. Document Why ​

Add comments to generated migrations:

php
/**
 * Migration generated from existing 'users' table
 * Generated on: 2024-01-15 12:00:00
 * Source database: production_v1
 */
return new class extends Migration { ... };

4. Test Generated Migrations ​

Always test generated migrations on a clean database:

php
// Run generated migration
$runner->run();

// Verify structure matches original
$newColumns = $inspector->getColumns('users');
// Compare with original...

Next Steps ​

Released under the MIT License.