feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -11,13 +11,14 @@ use App\Framework\Database\Schema\Commands\{
RawCommand,
RenameColumnCommand
};
use App\Framework\Database\ValueObjects\TableName;
/**
* Fluent table builder for defining table structures
*/
final class Blueprint
{
public readonly string $table;
public readonly TableName $table;
public array $columns = [];
@@ -37,9 +38,18 @@ final class Blueprint
public bool $ifNotExists = false;
public function __construct(string $table)
/**
* Table partitioning (PostgreSQL)
*/
public ?PartitionType $partitionType = null;
public array $partitionColumns = [];
public array $partitions = [];
public function __construct(string|TableName $table)
{
$this->table = $table;
$this->table = $table instanceof TableName ? $table : TableName::fromString($table);
}
/**
@@ -95,7 +105,7 @@ final class Blueprint
public function uuid(string $column = 'uuid'): ColumnDefinition
{
return $this->string($column, 36)->unique();
return $this->addColumn('uuid', $column);
}
public function increments(string $column): ColumnDefinition
@@ -234,6 +244,85 @@ final class Blueprint
return $this->addColumn('enum', $column, compact('allowed'));
}
/**
* Array column types (PostgreSQL)
*/
public function integerArray(string $column): ColumnDefinition
{
return $this->addColumn('integerArray', $column);
}
public function bigIntegerArray(string $column): ColumnDefinition
{
return $this->addColumn('bigIntegerArray', $column);
}
public function textArray(string $column): ColumnDefinition
{
return $this->addColumn('textArray', $column);
}
public function varcharArray(string $column, int $length = 255): ColumnDefinition
{
return $this->addColumn('varcharArray', $column, compact('length'));
}
public function uuidArray(string $column): ColumnDefinition
{
return $this->addColumn('uuidArray', $column);
}
public function jsonbArray(string $column): ColumnDefinition
{
return $this->addColumn('jsonbArray', $column);
}
public function timestampArray(string $column, int $precision = 0): ColumnDefinition
{
return $this->addColumn('timestampArray', $column, compact('precision'));
}
/**
* Full-text search columns (PostgreSQL)
*/
public function tsvector(string $column): ColumnDefinition
{
return $this->addColumn('tsvector', $column);
}
/**
* Range column types (PostgreSQL)
*/
public function int4range(string $column): ColumnDefinition
{
return $this->addColumn('int4range', $column);
}
public function int8range(string $column): ColumnDefinition
{
return $this->addColumn('int8range', $column);
}
public function numrange(string $column): ColumnDefinition
{
return $this->addColumn('numrange', $column);
}
public function tsrange(string $column): ColumnDefinition
{
return $this->addColumn('tsrange', $column);
}
public function tstzrange(string $column): ColumnDefinition
{
return $this->addColumn('tstzrange', $column);
}
public function daterange(string $column): ColumnDefinition
{
return $this->addColumn('daterange', $column);
}
/**
* Index definitions
*/
@@ -272,6 +361,24 @@ final class Blueprint
return $this;
}
public function ginIndex(string|array $columns, ?string $name = null): self
{
$index = new IndexDefinition($name, (array) $columns, IndexType::INDEX);
$index->using('GIN');
$this->indexes[] = $index;
return $this;
}
public function gistIndex(string|array $columns, ?string $name = null): self
{
$index = new IndexDefinition($name, (array) $columns, IndexType::INDEX);
$index->using('GIST');
$this->indexes[] = $index;
return $this;
}
/**
* Foreign key definitions
*/
@@ -286,14 +393,14 @@ final class Blueprint
/**
* Column operations
*/
public function dropColumn(string|array $columns): self
public function dropColumn(string|ColumnName ...$columns): self
{
$this->commands[] = new DropColumnCommand((array) $columns);
$this->commands[] = new DropColumnCommand(...$columns);
return $this;
}
public function renameColumn(string $from, string $to): self
public function renameColumn(string|ColumnName $from, string|ColumnName $to): self
{
$this->commands[] = new RenameColumnCommand($from, $to);
@@ -303,30 +410,30 @@ final class Blueprint
/**
* Index operations
*/
public function dropIndex(string|array $index): self
public function dropIndex(string|IndexName|ColumnName ...$index): self
{
$this->commands[] = new DropIndexCommand(...$index);
return $this;
}
public function dropUnique(string|IndexName|ColumnName ...$index): self
{
$this->commands[] = new DropIndexCommand(...$index);
return $this;
}
public function dropPrimary(string|IndexName $index = 'primary'): self
{
$this->commands[] = new DropIndexCommand($index);
return $this;
}
public function dropUnique(string|array $index): self
public function dropForeign(string|ConstraintName|ColumnName ...$index): self
{
$this->commands[] = new DropIndexCommand($index);
return $this;
}
public function dropPrimary(string $index = 'primary'): self
{
$this->commands[] = new DropIndexCommand($index);
return $this;
}
public function dropForeign(string|array $index): self
{
$this->commands[] = new DropForeignCommand($index);
$this->commands[] = new DropForeignCommand(...$index);
return $this;
}
@@ -341,6 +448,40 @@ final class Blueprint
return $this;
}
/**
* Table Partitioning (PostgreSQL)
*/
public function partitionByRange(string|array $columns): self
{
$this->partitionType = PartitionType::RANGE;
$this->partitionColumns = (array) $columns;
return $this;
}
public function partitionByList(string|array $columns): self
{
$this->partitionType = PartitionType::LIST;
$this->partitionColumns = (array) $columns;
return $this;
}
public function partitionByHash(string|array $columns): self
{
$this->partitionType = PartitionType::HASH;
$this->partitionColumns = (array) $columns;
return $this;
}
public function addPartition(Partition $partition): self
{
$this->partitions[] = $partition;
return $this;
}
private function addColumn(string $type, string $name, array $parameters = []): ColumnDefinition
{
$column = new ColumnDefinition($type, $name, $parameters);

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Framework\Database\Schema;
use App\Framework\Database\ValueObjects\ColumnName;
/**
* Column definition with fluent modifiers
*/
@@ -11,7 +13,7 @@ final class ColumnDefinition
{
public readonly string $type;
public readonly string $name;
public readonly ColumnName $name;
public readonly array $parameters;
@@ -41,10 +43,14 @@ final class ColumnDefinition
public ?string $collation = null;
public function __construct(string $type, string $name, array $parameters = [])
public bool $isGenerated = false;
public ?string $generatedExpression = null;
public function __construct(string $type, string|ColumnName $name, array $parameters = [])
{
$this->type = $type;
$this->name = $name;
$this->name = $name instanceof ColumnName ? $name : ColumnName::fromString($name);
$this->parameters = $parameters;
}
@@ -71,6 +77,11 @@ final class ColumnDefinition
return $this->default('CURRENT_TIMESTAMP');
}
public function uuidDefault(): self
{
return $this->default('gen_random_uuid()');
}
public function autoIncrement(): self
{
$this->autoIncrement = true;
@@ -140,4 +151,12 @@ final class ColumnDefinition
return $this;
}
public function storedAs(string $expression): self
{
$this->isGenerated = true;
$this->generatedExpression = $expression;
return $this;
}
}

View File

@@ -4,12 +4,18 @@ declare(strict_types=1);
namespace App\Framework\Database\Schema\Commands;
use App\Framework\Database\ValueObjects\ColumnName;
final class DropColumnCommand
{
/** @var array<ColumnName> */
public readonly array $columns;
public function __construct(array $columns)
public function __construct(string|ColumnName ...$columns)
{
$this->columns = $columns;
$this->columns = array_map(
fn($col) => $col instanceof ColumnName ? $col : ColumnName::fromString($col),
$columns
);
}
}

View File

@@ -4,12 +4,28 @@ declare(strict_types=1);
namespace App\Framework\Database\Schema\Commands;
use App\Framework\Database\ValueObjects\ColumnName;
use App\Framework\Database\ValueObjects\ConstraintName;
final class DropForeignCommand
{
public readonly string|array $index;
/** @var ConstraintName|array<ColumnName> */
public readonly ConstraintName|array $index;
public function __construct(string|array $index)
public function __construct(string|ConstraintName|ColumnName ...$index)
{
$this->index = $index;
// Single constraint name
if (count($index) === 1 && ($index[0] instanceof ConstraintName || is_string($index[0]))) {
$this->index = $index[0] instanceof ConstraintName
? $index[0]
: ConstraintName::fromString($index[0]);
}
// Multiple column names (array of columns for composite foreign keys)
else {
$this->index = array_map(
fn($col) => $col instanceof ColumnName ? $col : ColumnName::fromString($col),
$index
);
}
}
}

View File

@@ -4,12 +4,28 @@ declare(strict_types=1);
namespace App\Framework\Database\Schema\Commands;
use App\Framework\Database\ValueObjects\ColumnName;
use App\Framework\Database\ValueObjects\IndexName;
final class DropIndexCommand
{
public readonly string|array $index;
/** @var IndexName|array<ColumnName> */
public readonly IndexName|array $index;
public function __construct(string|array $index)
public function __construct(string|IndexName|ColumnName ...$index)
{
$this->index = $index;
// Single index name
if (count($index) === 1 && ($index[0] instanceof IndexName || is_string($index[0]))) {
$this->index = $index[0] instanceof IndexName
? $index[0]
: IndexName::fromString($index[0]);
}
// Multiple column names (array of columns for composite indexes)
else {
$this->index = array_map(
fn($col) => $col instanceof ColumnName ? $col : ColumnName::fromString($col),
$index
);
}
}
}

View File

@@ -4,15 +4,17 @@ declare(strict_types=1);
namespace App\Framework\Database\Schema\Commands;
use App\Framework\Database\ValueObjects\TableName;
final class DropTableCommand
{
public readonly string $table;
public readonly TableName $table;
public readonly bool $ifExists;
public function __construct(string $table, bool $ifExists = false)
public function __construct(string|TableName $table, bool $ifExists = false)
{
$this->table = $table;
$this->table = $table instanceof TableName ? $table : TableName::fromString($table);
$this->ifExists = $ifExists;
}
}

View File

@@ -4,15 +4,17 @@ declare(strict_types=1);
namespace App\Framework\Database\Schema\Commands;
use App\Framework\Database\ValueObjects\ColumnName;
final class RenameColumnCommand
{
public readonly string $from;
public readonly ColumnName $from;
public readonly string $to;
public readonly ColumnName $to;
public function __construct(string $from, string $to)
public function __construct(string|ColumnName $from, string|ColumnName $to)
{
$this->from = $from;
$this->to = $to;
$this->from = $from instanceof ColumnName ? $from : ColumnName::fromString($from);
$this->to = $to instanceof ColumnName ? $to : ColumnName::fromString($to);
}
}

View File

@@ -4,15 +4,17 @@ declare(strict_types=1);
namespace App\Framework\Database\Schema\Commands;
use App\Framework\Database\ValueObjects\TableName;
final class RenameTableCommand
{
public readonly string $from;
public readonly TableName $from;
public readonly string $to;
public readonly TableName $to;
public function __construct(string $from, string $to)
public function __construct(string|TableName $from, string|TableName $to)
{
$this->from = $from;
$this->to = $to;
$this->from = $from instanceof TableName ? $from : TableName::fromString($from);
$this->to = $to instanceof TableName ? $to : TableName::fromString($to);
}
}

View File

@@ -4,35 +4,55 @@ declare(strict_types=1);
namespace App\Framework\Database\Schema;
use App\Framework\Database\ValueObjects\ColumnName;
use App\Framework\Database\ValueObjects\ConstraintName;
use App\Framework\Database\ValueObjects\TableName;
final class ForeignKeyDefinition
{
/** @var array<ColumnName> */
public readonly array $columns;
public ?string $referencedTable = null;
public ?TableName $referencedTable = null;
/** @var array<ColumnName> */
public array $referencedColumns = [];
public ForeignKeyAction $onUpdate = ForeignKeyAction::RESTRICT;
public ForeignKeyAction $onDelete = ForeignKeyAction::RESTRICT;
public ?string $name = null;
public ?ConstraintName $name = null;
/**
* @param array<string|ColumnName> $columns
*/
public function __construct(array $columns)
{
$this->columns = $columns;
$this->columns = array_map(
fn($col) => $col instanceof ColumnName ? $col : ColumnName::fromString($col),
$columns
);
}
public function references(string|array $columns): self
/**
* @param string|ColumnName|array<string|ColumnName> $columns
*/
public function references(string|ColumnName|array $columns): self
{
$this->referencedColumns = (array) $columns;
$columnsArray = is_array($columns) ? $columns : [$columns];
$this->referencedColumns = array_map(
fn($col) => $col instanceof ColumnName ? $col : ColumnName::fromString($col),
$columnsArray
);
return $this;
}
public function on(string $table): self
public function on(string|TableName $table): self
{
$this->referencedTable = $table;
$this->referencedTable = $table instanceof TableName ? $table : TableName::fromString($table);
return $this;
}
@@ -71,9 +91,9 @@ final class ForeignKeyDefinition
return $this->onDelete(ForeignKeyAction::RESTRICT);
}
public function name(string $name): self
public function name(string|ConstraintName $name): self
{
$this->name = $name;
$this->name = $name instanceof ConstraintName ? $name : ConstraintName::fromString($name);
return $this;
}

View File

@@ -4,18 +4,42 @@ declare(strict_types=1);
namespace App\Framework\Database\Schema;
final readonly class IndexDefinition
use App\Framework\Database\ValueObjects\ColumnName;
use App\Framework\Database\ValueObjects\IndexName;
final class IndexDefinition
{
public ?string $name;
public readonly ?IndexName $name;
public array $columns;
/** @var array<ColumnName> */
public readonly array $columns;
public IndexType $type;
public readonly IndexType $type;
public function __construct(?string $name, array $columns, IndexType $type)
public ?string $using = null;
/**
* @param string|IndexName|null $name
* @param array<string|ColumnName> $columns
*/
public function __construct(string|IndexName|null $name, array $columns, IndexType $type)
{
$this->name = $name;
$this->columns = $columns;
$this->name = $name instanceof IndexName
? $name
: ($name !== null ? IndexName::fromString($name) : null);
$this->columns = array_map(
fn($col) => $col instanceof ColumnName ? $col : ColumnName::fromString($col),
$columns
);
$this->type = $type;
}
public function using(string $method): self
{
$this->using = strtoupper($method);
return $this;
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Schema;
/**
* Materialized View Definition für PostgreSQL
*
* Materialized Views sind Views mit physisch gespeicherten Ergebnissen:
* - Schneller als normale Views (keine Re-Computation)
* - Unterstützen Indexes
* - Müssen manuell oder geplant refreshed werden
*/
final readonly class MaterializedView
{
public function __construct(
public string $name,
public string $query,
public bool $withData = true
) {}
/**
* Generiert CREATE MATERIALIZED VIEW SQL
*/
public function toSql(): string
{
$sql = "CREATE MATERIALIZED VIEW \"{$this->name}\" AS {$this->query}";
if (!$this->withData) {
$sql .= " WITH NO DATA";
}
return $sql;
}
/**
* Generiert REFRESH MATERIALIZED VIEW SQL
*/
public function refreshSql(bool $concurrently = false): string
{
$sql = "REFRESH MATERIALIZED VIEW";
if ($concurrently) {
$sql .= " CONCURRENTLY";
}
$sql .= " \"{$this->name}\"";
return $sql;
}
/**
* Generiert DROP MATERIALIZED VIEW SQL
*/
public function dropSql(bool $ifExists = false): string
{
$sql = "DROP MATERIALIZED VIEW";
if ($ifExists) {
$sql .= " IF EXISTS";
}
$sql .= " \"{$this->name}\"";
return $sql;
}
}

View File

@@ -47,7 +47,7 @@ final class MySQLSchemaCompiler implements SchemaCompiler
$sql .= " IF NOT EXISTS";
}
$sql .= " `{$command->table}` (";
$sql .= " `{$blueprint->table}` (";
// Columns
$columns = [];
@@ -197,7 +197,7 @@ final class MySQLSchemaCompiler implements SchemaCompiler
// Add columns
foreach ($blueprint->columns as $column) {
$sql = "ALTER TABLE `{$command->table}` ADD COLUMN " . $this->compileColumn($column);
$sql = "ALTER TABLE `{$blueprint->table}` ADD COLUMN " . $this->compileColumn($column);
if ($column->after) {
$sql .= " AFTER `{$column->after}`";

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Schema;
/**
* Table Partition Definition
*
* Represents a partition of a partitioned table.
*
* Example Usage:
* - Range: Partition::range('sales_2024_q1', 'FOR VALUES FROM (\'2024-01-01\') TO (\'2024-04-01\')')
* - List: Partition::list('customers_eu', 'FOR VALUES IN (\'DE\', \'FR\', \'IT\')')
* - Hash: Partition::hash('orders_p0', 'FOR VALUES WITH (MODULUS 4, REMAINDER 0)')
*/
final readonly class Partition
{
private function __construct(
public string $name,
public string $forValues
) {}
public static function range(string $name, string $forValues): self
{
return new self($name, $forValues);
}
public static function list(string $name, string $forValues): self
{
return new self($name, $forValues);
}
public static function hash(string $name, string $forValues): self
{
return new self($name, $forValues);
}
public function toSql(string $parentTable): string
{
return "CREATE TABLE \"{$this->name}\" PARTITION OF \"{$parentTable}\" {$this->forValues}";
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Schema;
/**
* PostgreSQL Table Partition Types
*
* - RANGE: Partition by value ranges (dates, numbers)
* - LIST: Partition by discrete values (regions, categories)
* - HASH: Partition by hash value (distributed load)
*/
enum PartitionType: string
{
case RANGE = 'RANGE';
case LIST = 'LIST';
case HASH = 'HASH';
}

View File

@@ -49,7 +49,7 @@ final class PostgreSQLSchemaCompiler implements SchemaCompiler
$sql .= " IF NOT EXISTS";
}
$sql .= " \"{$command->table}\" (";
$sql .= " \"{$blueprint->table}\" (";
// Columns
$columns = [];
@@ -77,6 +77,12 @@ final class PostgreSQLSchemaCompiler implements SchemaCompiler
$sql .= implode(', ', $columns) . ")";
// Add partitioning if specified (PostgreSQL 10+)
if ($blueprint->partitionType !== null) {
$partitionColumns = '"' . implode('", "', $blueprint->partitionColumns) . '"';
$sql .= " PARTITION BY {$blueprint->partitionType->value} ({$partitionColumns})";
}
return $sql;
}
@@ -84,6 +90,12 @@ final class PostgreSQLSchemaCompiler implements SchemaCompiler
{
$sql = "\"{$column->name}\" " . $this->getColumnType($column);
// Generated columns
if ($column->isGenerated && $column->generatedExpression) {
$sql .= " GENERATED ALWAYS AS ({$column->generatedExpression}) STORED";
return $sql;
}
if (! $column->nullable) {
$sql .= " NOT NULL";
}
@@ -91,6 +103,8 @@ final class PostgreSQLSchemaCompiler implements SchemaCompiler
if ($column->hasDefault) {
if ($column->default === 'CURRENT_TIMESTAMP') {
$sql .= " DEFAULT CURRENT_TIMESTAMP";
} elseif ($column->default === 'gen_random_uuid()') {
$sql .= " DEFAULT gen_random_uuid()";
} else {
$sql .= " DEFAULT " . $this->quoteValue($column->default);
}
@@ -120,11 +134,29 @@ final class PostgreSQLSchemaCompiler implements SchemaCompiler
'binary' => 'BYTEA',
'json' => 'JSON',
'jsonb' => 'JSONB',
'uuid' => 'UUID',
'date' => 'DATE',
'dateTime' => $column->parameters['precision'] > 0 ? sprintf('TIMESTAMP(%d)', $column->parameters['precision']) : 'TIMESTAMP',
'time' => $column->parameters['precision'] > 0 ? sprintf('TIME(%d)', $column->parameters['precision']) : 'TIME',
'timestamp' => $column->parameters['precision'] > 0 ? sprintf('TIMESTAMP(%d)', $column->parameters['precision']) : 'TIMESTAMP',
'enum' => "VARCHAR(255) CHECK (\"{$column->name}\" IN ('" . implode("','", $column->parameters['allowed']) . "'))",
// Array types
'integerArray' => 'INTEGER[]',
'bigIntegerArray' => 'BIGINT[]',
'textArray' => 'TEXT[]',
'varcharArray' => sprintf('VARCHAR(%d)[]', $column->parameters['length'] ?? 255),
'uuidArray' => 'UUID[]',
'jsonbArray' => 'JSONB[]',
'timestampArray' => $column->parameters['precision'] > 0 ? sprintf('TIMESTAMP(%d)[]', $column->parameters['precision']) : 'TIMESTAMP[]',
// Full-text search types
'tsvector' => 'TSVECTOR',
// Range types
'int4range' => 'INT4RANGE',
'int8range' => 'INT8RANGE',
'numrange' => 'NUMRANGE',
'tsrange' => 'TSRANGE',
'tstzrange' => 'TSTZRANGE',
'daterange' => 'DATERANGE',
default => throw new \InvalidArgumentException("Unknown column type: {$column->type}")
};
}
@@ -158,21 +190,76 @@ final class PostgreSQLSchemaCompiler implements SchemaCompiler
// Add columns
foreach ($blueprint->columns as $column) {
$statements[] = "ALTER TABLE \"{$command->table}\" ADD COLUMN " . $this->compileColumn($column);
$statements[] = "ALTER TABLE \"{$blueprint->table}\" ADD COLUMN " . $this->compileColumn($column);
}
// Process commands (simplified)
// Add indexes/constraints
foreach ($blueprint->indexes as $index) {
$columns = '"' . implode('", "', $index->columns) . '"';
$indexName = $index->name ?: $this->generateIndexName((string)$blueprint->table, array_map('strval', $index->columns), $index->type);
$statement = match($index->type) {
IndexType::PRIMARY => "ALTER TABLE \"{$blueprint->table}\" ADD PRIMARY KEY ({$columns})",
IndexType::UNIQUE => "ALTER TABLE \"{$blueprint->table}\" ADD CONSTRAINT \"{$indexName}\" UNIQUE ({$columns})",
IndexType::INDEX => (function() use ($index, $indexName, $blueprint, $columns) {
$sql = "CREATE INDEX \"{$indexName}\" ON \"{$blueprint->table}\"";
if ($index->using) {
$sql .= " USING {$index->using}";
}
$sql .= " ({$columns})";
return $sql;
})(),
IndexType::FULLTEXT => throw new \RuntimeException("PostgreSQL does not support FULLTEXT indexes via ALTER TABLE"),
IndexType::SPATIAL => (function() use ($index, $indexName, $blueprint, $columns) {
$sql = "CREATE INDEX \"{$indexName}\" ON \"{$blueprint->table}\"";
// Use explicit USING if set, otherwise default to GIST for spatial indexes
$sql .= " USING " . ($index->using ?: 'GIST');
$sql .= " ({$columns})";
return $sql;
})(),
};
$statements[] = $statement;
}
// Process commands
foreach ($blueprint->commands as $cmd) {
if ($cmd instanceof DropColumnCommand) {
foreach ($cmd->columns as $col) {
$statements[] = "ALTER TABLE \"{$command->table}\" DROP COLUMN \"{$col}\"";
$statements[] = "ALTER TABLE \"{$blueprint->table}\" DROP COLUMN \"{$col}\"";
}
} elseif ($cmd instanceof DropIndexCommand) {
$indexName = is_array($cmd->index) ? $cmd->index[0] : $cmd->index;
// Try to drop as constraint first (for UNIQUE/PRIMARY KEY)
$statements[] = "ALTER TABLE \"{$blueprint->table}\" DROP CONSTRAINT IF EXISTS \"{$indexName}\"";
// Also try to drop as index (for regular INDEX)
$statements[] = "DROP INDEX IF EXISTS \"{$indexName}\"";
} elseif ($cmd instanceof RenameColumnCommand) {
$statements[] = "ALTER TABLE \"{$blueprint->table}\" RENAME COLUMN \"{$cmd->from}\" TO \"{$cmd->to}\"";
} elseif ($cmd instanceof DropForeignCommand) {
$foreignName = is_array($cmd->index) ? $cmd->index[0] : $cmd->index;
$statements[] = "ALTER TABLE \"{$blueprint->table}\" DROP CONSTRAINT IF EXISTS \"{$foreignName}\"";
}
}
return $statements;
}
private function generateIndexName(string $table, array $columns, IndexType $type): string
{
$prefix = match($type) {
IndexType::PRIMARY => 'pk',
IndexType::UNIQUE => 'uk',
IndexType::INDEX => 'idx',
IndexType::FULLTEXT => 'ft',
IndexType::SPATIAL => 'sp',
};
return $prefix . '_' . $table . '_' . implode('_', $columns);
}
private function compileDropTable(DropTableCommand $command): string
{
return $command->ifExists

View File

@@ -49,7 +49,7 @@ final class SQLiteSchemaCompiler implements SchemaCompiler
$sql .= " IF NOT EXISTS";
}
$sql .= " `{$command->table}` (";
$sql .= " `{$blueprint->table}` (";
// Columns
$columns = [];
@@ -147,13 +147,13 @@ final class SQLiteSchemaCompiler implements SchemaCompiler
// Add columns (SQLite supports this)
foreach ($blueprint->columns as $column) {
$statements[] = "ALTER TABLE `{$command->table}` ADD COLUMN " . $this->compileColumn($column);
$statements[] = "ALTER TABLE `{$blueprint->table}` ADD COLUMN " . $this->compileColumn($column);
}
// For drop/rename operations, we'd need table recreation
foreach ($blueprint->commands as $cmd) {
if ($cmd instanceof RenameColumnCommand) {
$statements[] = "ALTER TABLE `{$command->table}` RENAME COLUMN `{$cmd->from}` TO `{$cmd->to}`";
$statements[] = "ALTER TABLE `{$blueprint->table}` RENAME COLUMN `{$cmd->from}` TO `{$cmd->to}`";
}
// Drop column would need table recreation in older SQLite versions
}

View File

@@ -174,6 +174,7 @@ final class Schema
$this->connection->commit();
} catch (\Throwable $e) {
$this->connection->rollback();
throw $e;
}
}