PHP Classes

File: tests/Unit/Diff/SchemaDifferTest.php

Recommend this page to a friend!
  Packages of Alberto Arena   Laravel Truss   tests/Unit/Diff/SchemaDifferTest.php   Download  
File: tests/Unit/Diff/SchemaDifferTest.php
Role: Example script
Content type: text/plain
Description: Example script
Class: Laravel Truss
View a diagram of a Laravel application models
Author: By
Last change:
Date: 9 days ago
Size: 9,265 bytes
 

Contents

Class file image Download
<?php

declare(strict_types=1);

use
AlbertoArena\Truss\Diff\SchemaDiffer;

// The differ is pure: it compares two already-serialized snapshots (the shape
// SchemaSerializer produces) and returns a plain diff array. No database, no
// framework, no I/O. These fixtures build that array shape directly.

function sdCol(string $name, string $type = 'varchar(255)', bool $nullable = false, mixed $default = null): array
{
    return [
'name' => $name, 'type' => $type, 'nullable' => $nullable, 'default' => $default];
}

function
sdTable(string $name, array $columns = [], array $primaryKey = [], array $indexes = [], array $foreignKeys = []): array
{
    return [
       
'name' => $name,
       
'columns' => $columns,
       
'primary_key' => $primaryKey,
       
'indexes' => $indexes,
       
'foreign_keys' => $foreignKeys,
    ];
}

function
sdSnap(array $tables, ?string $generatedAt = '2026-07-29T10:00:00+00:00'): array
{
    return [
'connection' => 'testing', 'generated_at' => $generatedAt, 'tables' => $tables];
}

function
sdChangedTable(array $diff, string $name): ?array
{
    foreach (
$diff['tables_changed'] as $table) {
        if (
$table['name'] === $name) {
            return
$table;
        }
    }

    return
null;
}

it('reports no changes for identical snapshots', function () {
   
$users = sdTable('users', [sdCol('id', 'bigint unsigned'), sdCol('email')], ['id']);
   
$snap = sdSnap([$users]);

   
$diff = (new SchemaDiffer)->diff($snap, $snap);

   
expect($diff['has_changes'])->toBeFalse()
        ->
and($diff['tables_added'])->toBe([])
        ->
and($diff['tables_removed'])->toBe([])
        ->
and($diff['tables_changed'])->toBe([]);
});

it('echoes the two envelope timestamps', function () {
   
$baseline = sdSnap([], '2026-07-01T00:00:00+00:00');
   
$current = sdSnap([], '2026-07-29T00:00:00+00:00');

   
$diff = (new SchemaDiffer)->diff($baseline, $current);

   
expect($diff['baseline_generated_at'])->toBe('2026-07-01T00:00:00+00:00')
        ->
and($diff['current_generated_at'])->toBe('2026-07-29T00:00:00+00:00');
});

it('detects an added table', function () {
   
$baseline = sdSnap([sdTable('users', [sdCol('id')], ['id'])]);
   
$current = sdSnap([
       
sdTable('users', [sdCol('id')], ['id']),
       
sdTable('posts', [sdCol('id')], ['id']),
    ]);

   
$diff = (new SchemaDiffer)->diff($baseline, $current);

   
expect($diff['has_changes'])->toBeTrue()
        ->
and($diff['tables_added'])->toBe([['name' => 'posts']])
        ->
and($diff['tables_removed'])->toBe([])
        ->
and($diff['tables_changed'])->toBe([]);
});

it('detects a removed table', function () {
   
$baseline = sdSnap([
       
sdTable('users', [sdCol('id')], ['id']),
       
sdTable('legacy', [sdCol('id')], ['id']),
    ]);
   
$current = sdSnap([sdTable('users', [sdCol('id')], ['id'])]);

   
$diff = (new SchemaDiffer)->diff($baseline, $current);

   
expect($diff['tables_removed'])->toBe([['name' => 'legacy']])
        ->
and($diff['tables_added'])->toBe([]);
});

it('treats every current table as added when the baseline is empty', function () {
   
$current = sdSnap([sdTable('users', [sdCol('id')], ['id']), sdTable('posts')]);

   
$diff = (new SchemaDiffer)->diff(sdSnap([]), $current);

   
expect($diff['tables_added'])->toBe([['name' => 'users'], ['name' => 'posts']])
        ->
and($diff['has_changes'])->toBeTrue();
});

it('detects an added column with its full definition', function () {
   
$baseline = sdSnap([sdTable('users', [sdCol('id', 'bigint unsigned')], ['id'])]);
   
$current = sdSnap([sdTable('users', [
       
sdCol('id', 'bigint unsigned'),
       
sdCol('email', 'varchar(255)', true, null),
    ], [
'id'])]);

   
$diff = (new SchemaDiffer)->diff($baseline, $current);
   
$users = sdChangedTable($diff, 'users');

   
expect($users)->not->toBeNull()
        ->
and($users['columns_added'])->toBe([
            [
'name' => 'email', 'type' => 'varchar(255)', 'nullable' => true, 'default' => null],
        ])
        ->
and($users['columns_removed'])->toBe([])
        ->
and($users['columns_changed'])->toBe([]);
});

it('detects a removed column by name', function () {
   
$baseline = sdSnap([sdTable('users', [sdCol('id'), sdCol('nickname')], ['id'])]);
   
$current = sdSnap([sdTable('users', [sdCol('id')], ['id'])]);

   
$users = sdChangedTable((new SchemaDiffer)->diff($baseline, $current), 'users');

   
expect($users['columns_removed'])->toBe([['name' => 'nickname']]);
});

it('detects a column type change with before and after', function () {
   
$baseline = sdSnap([sdTable('users', [sdCol('id'), sdCol('votes', 'integer')], ['id'])]);
   
$current = sdSnap([sdTable('users', [sdCol('id'), sdCol('votes', 'bigint')], ['id'])]);

   
$users = sdChangedTable((new SchemaDiffer)->diff($baseline, $current), 'users');

   
expect($users['columns_changed'])->toBe([
        [
'name' => 'votes', 'changes' => ['type' => ['before' => 'integer', 'after' => 'bigint']]],
    ]);
});

it('detects nullable and default changes, only for the fields that moved', function () {
   
$baseline = sdSnap([sdTable('users', [sdCol('status', 'varchar(255)', false, 'draft')])]);
   
$current = sdSnap([sdTable('users', [sdCol('status', 'varchar(255)', true, 'active')])]);

   
$users = sdChangedTable((new SchemaDiffer)->diff($baseline, $current), 'users');

   
expect($users['columns_changed'])->toBe([
        [
'name' => 'status', 'changes' => [
           
'nullable' => ['before' => false, 'after' => true],
           
'default' => ['before' => 'draft', 'after' => 'active'],
        ]],
    ]);
});

it('does not list an unchanged table under tables_changed', function () {
   
$users = sdTable('users', [sdCol('id')], ['id']);
   
$baseline = sdSnap([$users, sdTable('posts', [sdCol('id')], ['id'])]);
   
$current = sdSnap([$users, sdTable('posts', [sdCol('id'), sdCol('title')], ['id'])]);

   
$diff = (new SchemaDiffer)->diff($baseline, $current);

   
expect($diff['tables_changed'])->toHaveCount(1)
        ->
and($diff['tables_changed'][0]['name'])->toBe('posts')
        ->
and(sdChangedTable($diff, 'users'))->toBeNull();
});

it('detects added, removed and changed indexes including composite columns', function () {
   
$baseline = sdSnap([sdTable('users', [sdCol('id'), sdCol('email'), sdCol('team_id')], ['id'], [
        [
'name' => 'users_email_index', 'columns' => ['email'], 'unique' => false],
        [
'name' => 'users_stale_index', 'columns' => ['team_id'], 'unique' => false],
    ])]);
   
$current = sdSnap([sdTable('users', [sdCol('id'), sdCol('email'), sdCol('team_id')], ['id'], [
        [
'name' => 'users_email_index', 'columns' => ['email'], 'unique' => true],
        [
'name' => 'users_team_id_name_index', 'columns' => ['team_id', 'email'], 'unique' => false],
    ])]);

   
$users = sdChangedTable((new SchemaDiffer)->diff($baseline, $current), 'users');

   
expect($users['indexes_added'])->toBe([
        [
'name' => 'users_team_id_name_index', 'columns' => ['team_id', 'email'], 'unique' => false],
    ])
        ->
and($users['indexes_removed'])->toBe([['name' => 'users_stale_index']])
        ->
and($users['indexes_changed'])->toBe([
            [
'name' => 'users_email_index', 'changes' => ['unique' => ['before' => false, 'after' => true]]],
        ]);
});

it('detects added, removed and changed foreign keys including composite columns', function () {
   
$fk = fn (string $name, array $cols, string $refTable, array $refCols, string $onDelete = 'cascade'): array => [
       
'name' => $name,
       
'columns' => $cols,
       
'references_table' => $refTable,
       
'references_columns' => $refCols,
       
'on_update' => 'no action',
       
'on_delete' => $onDelete,
    ];

   
$baseline = sdSnap([sdTable('posts', [sdCol('id'), sdCol('user_id'), sdCol('team_id')], ['id'], [], [
       
$fk('posts_user_id_foreign', ['user_id'], 'users', ['id'], 'cascade'),
       
$fk('posts_team_id_foreign', ['team_id'], 'teams', ['id'], 'cascade'),
    ])]);
   
$current = sdSnap([sdTable('posts', [sdCol('id'), sdCol('user_id'), sdCol('team_id')], ['id'], [], [
       
$fk('posts_user_id_foreign', ['user_id'], 'users', ['id'], 'set null'),
       
$fk('posts_team_slug_foreign', ['team_id', 'user_id'], 'memberships', ['team_id', 'user_id'], 'cascade'),
    ])]);

   
$posts = sdChangedTable((new SchemaDiffer)->diff($baseline, $current), 'posts');

   
expect($posts['foreign_keys_added'])->toBe([
       
$fk('posts_team_slug_foreign', ['team_id', 'user_id'], 'memberships', ['team_id', 'user_id'], 'cascade'),
    ])
        ->
and($posts['foreign_keys_removed'])->toBe([['name' => 'posts_team_id_foreign']])
        ->
and($posts['foreign_keys_changed'])->toBe([
            [
'name' => 'posts_user_id_foreign', 'changes' => ['on_delete' => ['before' => 'cascade', 'after' => 'set null']]],
        ]);
});

it('detects a primary key change', function () {
   
$baseline = sdSnap([sdTable('sessions', [sdCol('id'), sdCol('token')], ['id'])]);
   
$current = sdSnap([sdTable('sessions', [sdCol('id'), sdCol('token')], ['id', 'token'])]);

   
$sessions = sdChangedTable((new SchemaDiffer)->diff($baseline, $current), 'sessions');

   
expect($sessions['changes'] ?? null)->toBe(['primary_key' => ['before' => ['id'], 'after' => ['id', 'token']]]);
});