mapper = new SqlStateErrorMapper(); }); describe('mapToErrorCode()', function () { it('maps connection errors correctly', function () { $testCases = [ '08001' => DatabaseErrorCode::CONNECTION_FAILED, '08003' => DatabaseErrorCode::CONNECTION_FAILED, '08004' => DatabaseErrorCode::CONNECTION_FAILED, '08006' => DatabaseErrorCode::CONNECTION_FAILED, ]; foreach ($testCases as $sqlStateCode => $expectedErrorCode) { $sqlState = new SqlState($sqlStateCode); $errorCode = $this->mapper->mapToErrorCode($sqlState); expect($errorCode)->toBe($expectedErrorCode); } }); it('maps constraint violations correctly', function () { $codes = ['23000', '23502', '23503', '23505', '23514']; foreach ($codes as $code) { $sqlState = new SqlState($code); $errorCode = $this->mapper->mapToErrorCode($sqlState); expect($errorCode)->toBe(DatabaseErrorCode::CONSTRAINT_VIOLATION); } }); it('maps transaction errors correctly', function () { $deadlock = new SqlState('40001'); expect($this->mapper->mapToErrorCode($deadlock)) ->toBe(DatabaseErrorCode::DEADLOCK_DETECTED); $transactionError = new SqlState('40002'); expect($this->mapper->mapToErrorCode($transactionError)) ->toBe(DatabaseErrorCode::TRANSACTION_FAILED); }); it('maps syntax errors correctly', function () { $codes = ['42000', '42501', '42601', '42P01', '42703', '42S02', '42S22']; foreach ($codes as $code) { $sqlState = new SqlState($code); $errorCode = $this->mapper->mapToErrorCode($sqlState); expect($errorCode)->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR); } }); it('maps data exceptions correctly', function () { $codes = ['22001', '22003', '22007', '22012', '22P02']; foreach ($codes as $code) { $sqlState = new SqlState($code); $errorCode = $this->mapper->mapToErrorCode($sqlState); expect($errorCode)->toBe(DatabaseErrorCode::QUERY_EXECUTION_FAILED); } }); it('falls back to class-based mapping for unknown codes', function () { // Unknown code in connection class $unknownConnection = new SqlState('08999'); expect($this->mapper->mapToErrorCode($unknownConnection)) ->toBe(DatabaseErrorCode::CONNECTION_FAILED); // Unknown code in constraint class $unknownConstraint = new SqlState('23999'); expect($this->mapper->mapToErrorCode($unknownConstraint)) ->toBe(DatabaseErrorCode::CONSTRAINT_VIOLATION); // Unknown code in transaction class $unknownTransaction = new SqlState('40999'); expect($this->mapper->mapToErrorCode($unknownTransaction)) ->toBe(DatabaseErrorCode::TRANSACTION_FAILED); // Unknown code in syntax class $unknownSyntax = new SqlState('42999'); expect($this->mapper->mapToErrorCode($unknownSyntax)) ->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR); }); }); describe('getHumanReadableMessage()', function () { it('provides user-friendly messages for connection errors', function () { $sqlState = new SqlState('08001'); $message = $this->mapper->getHumanReadableMessage($sqlState); expect($message)->toContain('Unable to establish database connection'); expect($message)->toContain('Please check server status'); }); it('provides user-friendly messages for constraint violations', function () { $uniqueViolation = new SqlState('23505'); $message = $this->mapper->getHumanReadableMessage($uniqueViolation); expect($message)->toContain('Duplicate entry'); expect($message)->toContain('already exists'); }); it('provides user-friendly messages for deadlocks', function () { $deadlock = new SqlState('40001'); $message = $this->mapper->getHumanReadableMessage($deadlock); expect($message)->toContain('Deadlock detected'); expect($message)->toContain('retry'); }); it('provides user-friendly messages for syntax errors', function () { $tableNotFound = new SqlState('42S02'); $message = $this->mapper->getHumanReadableMessage($tableNotFound); expect($message)->toContain('table not found'); expect($message)->toContain('schema'); }); it('provides generic message for unknown codes', function () { $unknownCode = new SqlState('99999'); $message = $this->mapper->getHumanReadableMessage($unknownCode); expect($message)->toContain('Database error occurred'); expect($message)->toContain('99999'); }); it('includes SQLSTATE code in fallback messages', function () { $unknownCode = new SqlState('88888'); $message = $this->mapper->getHumanReadableMessage($unknownCode); expect($message)->toContain('88888'); }); }); describe('getTechnicalDetails()', function () { it('provides complete technical information', function () { $sqlState = new SqlState('23505'); $details = $this->mapper->getTechnicalDetails($sqlState); expect($details)->toHaveKey('sqlstate'); expect($details)->toHaveKey('class'); expect($details)->toHaveKey('subclass'); expect($details)->toHaveKey('category'); expect($details)->toHaveKey('error_code'); expect($details['sqlstate'])->toBe('23505'); expect($details['class'])->toBe('23'); expect($details['subclass'])->toBe('505'); expect($details['category'])->toBe('Integrity Constraint Violation'); expect($details['error_code'])->toBe('DB003'); }); it('categorizes different SQLSTATE classes', function () { $testCases = [ ['code' => '08001', 'category' => 'Connection Exception'], ['code' => '23505', 'category' => 'Integrity Constraint Violation'], ['code' => '40001', 'category' => 'Transaction Rollback'], ['code' => '42000', 'category' => 'Syntax Error or Access Violation'], ['code' => '22001', 'category' => 'Data Exception'], ]; foreach ($testCases as $testCase) { $sqlState = new SqlState($testCase['code']); $details = $this->mapper->getTechnicalDetails($sqlState); expect($details['category'])->toBe($testCase['category']); } }); }); describe('isRetryable()', function () { it('identifies retryable errors', function () { $retryableCodes = [ '40001', // Deadlock '08001', // Connection failed '08003', // Connection does not exist '08006', // Connection failure during transaction '40002', // Transaction integrity constraint '40003', // Statement completion unknown ]; foreach ($retryableCodes as $code) { $sqlState = new SqlState($code); expect($this->mapper->isRetryable($sqlState)) ->toBeTrue("SQLSTATE {$code} should be retryable"); } }); it('identifies non-retryable errors', function () { $nonRetryableCodes = [ '23505', // Unique constraint '42000', // Syntax error '22001', // String data truncation ]; foreach ($nonRetryableCodes as $code) { $sqlState = new SqlState($code); expect($this->mapper->isRetryable($sqlState)) ->toBeFalse("SQLSTATE {$code} should not be retryable"); } }); }); describe('getRetryDelay()', function () { it('recommends immediate retry for deadlocks', function () { $deadlock = new SqlState('40001'); $delay = $this->mapper->getRetryDelay($deadlock); expect($delay)->toBe(1); // 1 second for quick retry }); it('recommends longer delay for connection errors', function () { $connectionError = new SqlState('08001'); $delay = $this->mapper->getRetryDelay($connectionError); expect($delay)->toBe(30); // 30 seconds for connection recovery }); it('recommends moderate delay for transaction errors', function () { $transactionError = new SqlState('40002'); $delay = $this->mapper->getRetryDelay($transactionError); expect($delay)->toBe(5); // 5 seconds for transaction retry }); it('returns null for non-retryable errors', function () { $syntaxError = new SqlState('42000'); $delay = $this->mapper->getRetryDelay($syntaxError); expect($delay)->toBeNull(); }); it('provides default delay for retryable errors without specific mapping', function () { $unknownRetryable = new SqlState('08999'); // This should map to CONNECTION_FAILED via class-based fallback if ($this->mapper->isRetryable($unknownRetryable)) { $delay = $this->mapper->getRetryDelay($unknownRetryable); expect($delay)->toBeGreaterThan(0); } }); }); describe('PostgreSQL-specific behavior', function () { it('handles PostgreSQL unique violation', function () { $pgUniqueViolation = new SqlState('23505'); expect($this->mapper->mapToErrorCode($pgUniqueViolation)) ->toBe(DatabaseErrorCode::CONSTRAINT_VIOLATION); $message = $this->mapper->getHumanReadableMessage($pgUniqueViolation); expect($message)->toContain('Duplicate entry'); }); it('handles PostgreSQL table not found', function () { $pgTableNotFound = new SqlState('42P01'); expect($this->mapper->mapToErrorCode($pgTableNotFound)) ->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR); $message = $this->mapper->getHumanReadableMessage($pgTableNotFound); expect($message)->toContain('table does not exist'); }); }); describe('MySQL-specific behavior', function () { it('handles MySQL table not found', function () { $mysqlTableNotFound = new SqlState('42S02'); expect($this->mapper->mapToErrorCode($mysqlTableNotFound)) ->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR); $message = $this->mapper->getHumanReadableMessage($mysqlTableNotFound); expect($message)->toContain('table not found'); }); it('handles MySQL column not found', function () { $mysqlColumnNotFound = new SqlState('42S22'); expect($this->mapper->mapToErrorCode($mysqlColumnNotFound)) ->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR); $message = $this->mapper->getHumanReadableMessage($mysqlColumnNotFound); expect($message)->toContain('column not found'); }); }); describe('comprehensive error coverage', function () { it('covers all major SQLSTATE classes', function () { $classes = [ '08' => 'Connection', '23' => 'Constraint', '40' => 'Transaction', '42' => 'Syntax', '22' => 'Data', ]; foreach ($classes as $class => $description) { $sqlState = new SqlState($class . '000'); $errorCode = $this->mapper->mapToErrorCode($sqlState); expect($errorCode)->toBeInstanceOf(DatabaseErrorCode::class); } }); it('provides meaningful messages for all error types', function () { $testCodes = [ '08001', '23505', '40001', '42000', '22001' ]; foreach ($testCodes as $code) { $sqlState = new SqlState($code); $message = $this->mapper->getHumanReadableMessage($sqlState); expect($message)->not->toBeEmpty(); expect(strlen($message))->toBeGreaterThan(20); } }); }); });