withData(['table_name' => $tableName]); } $this->validateColumnName($versionColumn, 'version'); $this->validateColumnName($descriptionColumn, 'description'); $this->validateColumnName($executedAtColumn, 'executed_at'); $this->validateColumnName($idColumn, 'id'); } public static function default(): self { return new self('migrations'); } public static function withCustomTable(string $tableName): self { return new self($tableName); } /** * Get the full INSERT SQL for recording a migration */ public function getInsertSql(): string { return "INSERT INTO {$this->tableName} ({$this->versionColumn}, {$this->descriptionColumn}, {$this->executedAtColumn}) VALUES (?, ?, ?)"; } /** * Get the SELECT SQL for fetching applied versions */ public function getVersionSelectSql(): string { return "SELECT {$this->versionColumn} FROM {$this->tableName} ORDER BY {$this->executedAtColumn}"; } /** * Get the DELETE SQL for removing a migration record */ public function getDeleteSql(): string { return "DELETE FROM {$this->tableName} WHERE {$this->versionColumn} = ?"; } /** * Get the CREATE TABLE SQL for the given database driver */ public function getCreateTableSql(string $driver): string { return match($driver) { 'mysql' => "CREATE TABLE IF NOT EXISTS {$this->tableName} ( {$this->idColumn} INT PRIMARY KEY AUTO_INCREMENT, {$this->versionColumn} VARCHAR(20) NOT NULL UNIQUE COMMENT 'Format: YYYY_MM_DD_HHMMSS', {$this->descriptionColumn} TEXT, {$this->executedAtColumn} TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_{$this->versionColumn} ({$this->versionColumn}), INDEX idx_{$this->executedAtColumn} ({$this->executedAtColumn}) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", 'pgsql' => "CREATE TABLE IF NOT EXISTS {$this->tableName} ( {$this->idColumn} SERIAL PRIMARY KEY, {$this->versionColumn} VARCHAR(20) NOT NULL UNIQUE, {$this->descriptionColumn} TEXT, {$this->executedAtColumn} TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP )", 'sqlite' => "CREATE TABLE IF NOT EXISTS {$this->tableName} ( {$this->idColumn} INTEGER PRIMARY KEY AUTOINCREMENT, {$this->versionColumn} TEXT NOT NULL UNIQUE, {$this->descriptionColumn} TEXT, {$this->executedAtColumn} TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP )", default => throw FrameworkException::create( ErrorCode::VAL_UNSUPPORTED_OPERATION, "Unsupported database driver for migrations" )->withData(['driver' => $driver]) }; } /** * Get PostgreSQL index creation SQL (separate from table creation) */ public function getPostgreSqlIndexSql(): array { return [ "CREATE INDEX IF NOT EXISTS idx_{$this->tableName}_{$this->versionColumn} ON {$this->tableName} ({$this->versionColumn})", "CREATE INDEX IF NOT EXISTS idx_{$this->tableName}_{$this->executedAtColumn} ON {$this->tableName} ({$this->executedAtColumn})", ]; } private function validateColumnName(string $columnName, string $context): void { if (empty($columnName)) { throw FrameworkException::create( ErrorCode::VAL_BUSINESS_RULE_VIOLATION, "Migration {$context} column name cannot be empty" )->withData([ 'column_name' => $columnName, 'context' => $context, ]); } // Basic SQL injection prevention if (! preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $columnName)) { throw FrameworkException::create( ErrorCode::VAL_BUSINESS_RULE_VIOLATION, "Invalid column name format for migration {$context}" )->withData([ 'column_name' => $columnName, 'context' => $context, ]); } } }