Files
michaelschiemer/docs/database/new-features.md
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

9.6 KiB

Database Module New Features

This document provides an overview of the new features added to the Database module.

Table of Contents

Stored Procedures and Functions

The Database module now supports defining, executing, and managing stored procedures and functions across different database systems.

StoredProcedureDefinition

The StoredProcedureDefinition class provides a fluent interface for defining stored procedures:

use App\Framework\Database\StoredProcedure\StoredProcedureDefinition;

$procedure = StoredProcedureDefinition::create('get_user_by_id')
    ->withParameter('user_id', 'INT')
    ->withBody('SELECT * FROM users WHERE id = user_id')
    ->returnsType('TABLE');

The definition can then be converted to SQL for different database systems:

// Generate MySQL-specific SQL
$mysqlSql = $procedure->toSql('mysql');

// Generate PostgreSQL-specific SQL
$pgsqlSql = $procedure->toSql('pgsql');

StoredProcedureManager

The StoredProcedureManager class provides methods for managing stored procedures:

use App\Framework\Database\StoredProcedure\StoredProcedureManager;

// Create a new manager with a database connection
$manager = new StoredProcedureManager($connection);

// Create a stored procedure
$manager->createProcedure($procedure);

// Check if a procedure exists
if ($manager->procedureExists('get_user_by_id')) {
    // Execute the procedure with parameters
    $result = $manager->executeProcedure('get_user_by_id', [42]);
    
    // Process the results
    foreach ($result->fetchAll() as $row) {
        // ...
    }
}

// Execute a stored function
$value = $manager->executeFunction('calculate_order_total', [123, true]);

// Drop a procedure
$manager->dropProcedure('get_user_by_id');

// List all procedures
$procedures = $manager->listProcedures();

Extended Index Management

The Database module now supports advanced index types and features across different database systems.

AdvancedIndexDefinition

The AdvancedIndexDefinition class provides a fluent interface for defining advanced indexes:

use App\Framework\Database\Schema\Index\AdvancedIndexDefinition;
use App\Framework\Database\Schema\Index\AdvancedIndexType;

// Create a standard index
$index = AdvancedIndexDefinition::create(
    'idx_users_email',
    ['email'],
    AdvancedIndexType::INDEX
);

// Create a partial index (only indexes rows that match a condition)
$partialIndex = AdvancedIndexDefinition::partial(
    'idx_active_users',
    ['email'],
    AdvancedIndexType::INDEX,
    'active = true'
);

// Create a functional index (indexes the result of a function)
$functionalIndex = AdvancedIndexDefinition::functional(
    'idx_lower_email',
    AdvancedIndexType::INDEX,
    ['LOWER(email)']
);

// Create a PostgreSQL GIN index (for full-text search, JSON, arrays)
$ginIndex = AdvancedIndexDefinition::gin(
    'idx_document_content',
    ['content']
);

// Create a PostgreSQL GiST index (for geometric data, ranges)
$gistIndex = AdvancedIndexDefinition::gist(
    'idx_location_position',
    ['position']
);

// Create a MySQL BTREE index with options
$btreeIndex = AdvancedIndexDefinition::btree(
    'idx_users_name',
    ['first_name', 'last_name'],
    ['fillfactor' => 70]
);

The index definition can then be converted to SQL for different database systems:

// Generate SQL for PostgreSQL
$sql = $index->toSql('pgsql', 'users');

// Generate SQL for MySQL
$sql = $index->toSql('mysql', 'users');

Enhanced Schema Versioning

The Database module now includes enhanced schema versioning capabilities, including dependency tracking and schema comparison tools.

Schema Comparison

The schema comparison tools allow you to compare two database schemas and generate migration code to update one schema to match the other.

use App\Framework\Database\Schema\Comparison\SchemaComparator;

// Create a comparator with source and target connections
$comparator = new SchemaComparator($sourceConnection, $targetConnection);

// Compare the schemas
$difference = $comparator->compare();

// Check if there are differences
if ($difference->hasDifferences()) {
    // Get a summary of the differences
    $summary = $difference->getSummary();
    
    // Get a detailed description of the differences
    $description = $difference->getDescription();
    
    // Generate migration code to update the target schema to match the source schema
    $migrationCode = $difference->generateMigrationCode('UpdateSchema');
    
    // Save the migration code to a file
    file_put_contents('database/migrations/UpdateSchema.php', $migrationCode);
}

Dependent Migrations

The enhanced migration system now supports dependencies between migrations:

use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Migration\AbstractDependentMigration;
use App\Framework\Database\Migration\MigrationVersion;
use App\Framework\Database\Schema\Schema;

final class CreateCommentsTable extends AbstractDependentMigration
{
    public function getVersion(): MigrationVersion
    {
        return MigrationVersion::fromString('20250805123456');
    }
    
    public function getDescription(): string
    {
        return 'Create comments table';
    }
    
    public function getDependencies(): array
    {
        // This migration depends on the CreateUsersTable and CreatePostsTable migrations
        return [
            MigrationVersion::fromString('20250805123455'), // CreateUsersTable
            MigrationVersion::fromString('20250805123454')  // CreatePostsTable
        ];
    }
    
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        
        $schema->create('comments', function ($table) {
            $table->id();
            $table->text('content');
            $table->foreignId('user_id')->references('id')->on('users');
            $table->foreignId('post_id')->references('id')->on('posts');
            $table->timestamps();
        });
        
        $schema->execute();
    }
    
    public function down(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->dropIfExists('comments');
        $schema->execute();
    }
}

The migration runner will ensure that migrations are executed in the correct order based on their dependencies.

Usage Examples

Creating and Using a Stored Procedure

use App\Framework\Database\StoredProcedure\StoredProcedureDefinition;
use App\Framework\Database\StoredProcedure\StoredProcedureManager;

// Define a stored procedure to get active users
$procedure = StoredProcedureDefinition::create('get_active_users')
    ->withParameter('min_login_count', 'INT')
    ->withBody('
        SELECT * FROM users 
        WHERE active = true AND login_count >= min_login_count
        ORDER BY last_login DESC;
    ')
    ->returnsType('TABLE');

// Create the procedure in the database
$manager = new StoredProcedureManager($connection);
$manager->createProcedure($procedure);

// Execute the procedure
$result = $manager->executeProcedure('get_active_users', [5]);

// Process the results
foreach ($result->fetchAll() as $user) {
    echo "User: {$user['name']} (Last login: {$user['last_login']})\n";
}

Creating Advanced Indexes

use App\Framework\Database\Schema\Index\AdvancedIndexDefinition;
use App\Framework\Database\Schema\Index\AdvancedIndexType;

// In a migration
public function up(ConnectionInterface $connection): void
{
    $schema = new Schema($connection);
    
    $schema->create('posts', function ($table) {
        $table->id();
        $table->string('title');
        $table->text('content');
        $table->boolean('published')->default(false);
        $table->timestamps();
    });
    
    $schema->execute();
    
    // Create a partial index on published posts
    $partialIndex = AdvancedIndexDefinition::partial(
        'idx_published_posts',
        ['title'],
        AdvancedIndexType::INDEX,
        'published = true'
    );
    
    // Create a functional index for case-insensitive search
    $functionalIndex = AdvancedIndexDefinition::functional(
        'idx_lower_title',
        AdvancedIndexType::INDEX,
        ['LOWER(title)']
    );
    
    // Generate and execute SQL for the current database
    $driver = $connection->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
    $connection->execute($partialIndex->toSql($driver, 'posts'));
    $connection->execute($functionalIndex->toSql($driver, 'posts'));
}

Comparing Schemas and Generating Migrations

use App\Framework\Database\Schema\Comparison\SchemaComparator;

// Compare development and production schemas
$comparator = new SchemaComparator($devConnection, $prodConnection);
$difference = $comparator->compare();

if ($difference->hasDifferences()) {
    // Print a summary of the differences
    $summary = $difference->getSummary();
    echo "Found {$summary['total_differences']} differences:\n";
    echo "  Missing tables: {$summary['missing_tables']}\n";
    echo "  Extra tables: {$summary['extra_tables']}\n";
    echo "  Modified tables: {$summary['modified_tables']}\n";
    
    // Generate migration code
    $timestamp = date('YmdHis');
    $className = "UpdateProductionSchema{$timestamp}";
    $migrationCode = $difference->generateMigrationCode($className);
    
    // Save the migration
    $migrationPath = "database/migrations/{$className}.php";
    file_put_contents($migrationPath, $migrationCode);
    
    echo "Generated migration: {$migrationPath}\n";
}