Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

declare(strict_types=1);

namespace Flow\PostgreSql\Client\Exception;

final readonly class PostgreSqlError
{
private function __construct(
public string $sqlState,
public PostgreSqlErrorCategory $category,
public string $message,
public ?string $detail,
public ?string $hint,
public ?string $schema,
public ?string $table,
public ?string $column,
public ?string $constraint,
public ?int $position,
) {
}

public static function fromDiagnostics(
string $sqlState,
string $message,
?string $detail = null,
?string $hint = null,
?string $schema = null,
?string $table = null,
?string $column = null,
?string $constraint = null,
?int $position = null,
) : self {
return new self(
$sqlState,
PostgreSqlErrorCategory::fromSqlState($sqlState),
$message,
$detail,
$hint,
$schema,
$table,
$column,
$constraint,
$position,
);
}

public static function unknown(string $message = 'Unknown error') : self
{
return new self(
'00000',
PostgreSqlErrorCategory::UNKNOWN,
$message,
null,
null,
null,
null,
null,
null,
null,
);
}

public function fullMessage() : string
{
return $this->message;
}

public function isCheckViolation() : bool
{
return $this->sqlState === '23514';
}

public function isConnectionError() : bool
{
return $this->category === PostgreSqlErrorCategory::CONNECTION_EXCEPTION;
}

public function isDataError() : bool
{
return $this->category === PostgreSqlErrorCategory::DATA_EXCEPTION;
}

public function isDeadlockDetected() : bool
{
return $this->sqlState === '40P01';
}

public function isExclusionViolation() : bool
{
return $this->sqlState === '23P01';
}

public function isForeignKeyViolation() : bool
{
return $this->sqlState === '23503';
}

public function isIntegrityViolation() : bool
{
return $this->category === PostgreSqlErrorCategory::INTEGRITY_CONSTRAINT_VIOLATION;
}

public function isNotNullViolation() : bool
{
return $this->sqlState === '23502';
}

public function isSerializationFailure() : bool
{
return $this->sqlState === '40001';
}

public function isSyntaxError() : bool
{
return $this->category === PostgreSqlErrorCategory::SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION;
}

public function isTransactionRollback() : bool
{
return $this->category === PostgreSqlErrorCategory::TRANSACTION_ROLLBACK;
}

public function isUniqueViolation() : bool
{
return $this->sqlState === '23505';
}

public function safeMessage() : string
{
return $this->category->safeMessage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

declare(strict_types=1);

namespace Flow\PostgreSql\Client\Exception;

enum PostgreSqlErrorCategory : string
{
case CARDINALITY_VIOLATION = '21';
case CASE_NOT_FOUND = '20';
case CONFIGURATION_FILE_ERROR = 'F0';
case CONNECTION_EXCEPTION = '08';
case DATA_EXCEPTION = '22';
case DEPENDENT_PRIVILEGE_DESCRIPTORS = '2B';
case DIAGNOSTICS_EXCEPTION = '0Z';
case EXTERNAL_ROUTINE_EXCEPTION = '38';
case EXTERNAL_ROUTINE_INVOCATION_EXCEPTION = '39';
case FDW_ERROR = 'HV';
case FEATURE_NOT_SUPPORTED = '0A';
case INSUFFICIENT_RESOURCES = '53';
case INTEGRITY_CONSTRAINT_VIOLATION = '23';
case INTERNAL_ERROR = 'XX';
case INVALID_AUTHORIZATION_SPECIFICATION = '28';
case INVALID_CATALOG_NAME = '3D';
case INVALID_CURSOR_NAME = '34';
case INVALID_CURSOR_STATE = '24';
case INVALID_GRANTOR = '0L';
case INVALID_ROLE_SPECIFICATION = '0P';
case INVALID_SCHEMA_NAME = '3F';
case INVALID_SQL_STATEMENT_NAME = '26';
case INVALID_TRANSACTION_INITIATION = '0B';
case INVALID_TRANSACTION_STATE = '25';
case INVALID_TRANSACTION_TERMINATION = '2D';
case LOCATOR_EXCEPTION = '0F';
case NO_DATA = '02';
case OBJECT_NOT_IN_PREREQUISITE_STATE = '55';
case OPERATOR_INTERVENTION = '57';
case PL_PGSQL_ERROR = 'P0';
case PROGRAM_LIMIT_EXCEEDED = '54';
case SAVEPOINT_EXCEPTION = '3B';
case SNAPSHOT_FAILURE = '72';
case SQL_ROUTINE_EXCEPTION = '2F';
case SQL_STATEMENT_NOT_YET_COMPLETE = '03';
case SUCCESSFUL_COMPLETION = '00';
case SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION = '42';
case SYSTEM_ERROR = '58';
case TRANSACTION_ROLLBACK = '40';
case TRIGGERED_ACTION_EXCEPTION = '09';
case TRIGGERED_DATA_CHANGE_VIOLATION = '27';
case UNKNOWN = '??';
case WARNING = '01';
case WITH_CHECK_OPTION_VIOLATION = '44';

public static function fromSqlState(string $sqlState) : self
{
if (\strlen($sqlState) < 2) {
return self::UNKNOWN;
}

$class = \substr($sqlState, 0, 2);

return self::tryFrom($class) ?? self::UNKNOWN;
}

public function isRecoverable() : bool
{
return match ($this) {
self::TRANSACTION_ROLLBACK => true,
default => false,
};
}

public function safeMessage() : string
{
return match ($this) {
self::SUCCESSFUL_COMPLETION => 'Operation completed successfully',
self::WARNING => 'Operation completed with warnings',
self::NO_DATA => 'No data found',
self::SQL_STATEMENT_NOT_YET_COMPLETE => 'SQL statement not yet complete',
self::CONNECTION_EXCEPTION => 'Database connection error occurred',
self::TRIGGERED_ACTION_EXCEPTION => 'Triggered action error',
self::FEATURE_NOT_SUPPORTED => 'Feature not supported',
self::INVALID_TRANSACTION_INITIATION => 'Invalid transaction initiation',
self::LOCATOR_EXCEPTION => 'Locator error',
self::INVALID_GRANTOR => 'Invalid grantor',
self::INVALID_ROLE_SPECIFICATION => 'Invalid role specification',
self::DIAGNOSTICS_EXCEPTION => 'Diagnostics error',
self::CASE_NOT_FOUND => 'Case not found',
self::CARDINALITY_VIOLATION => 'Cardinality violation',
self::DATA_EXCEPTION => 'Invalid data format or value',
self::INTEGRITY_CONSTRAINT_VIOLATION => 'Data constraint violation',
self::INVALID_CURSOR_STATE => 'Invalid cursor state',
self::INVALID_TRANSACTION_STATE => 'Invalid transaction state',
self::INVALID_SQL_STATEMENT_NAME => 'Invalid SQL statement name',
self::TRIGGERED_DATA_CHANGE_VIOLATION => 'Triggered data change violation',
self::INVALID_AUTHORIZATION_SPECIFICATION => 'Authorization error',
self::DEPENDENT_PRIVILEGE_DESCRIPTORS => 'Dependent privilege descriptors still exist',
self::INVALID_TRANSACTION_TERMINATION => 'Invalid transaction termination',
self::SQL_ROUTINE_EXCEPTION => 'SQL routine error',
self::INVALID_CURSOR_NAME => 'Invalid cursor name',
self::EXTERNAL_ROUTINE_EXCEPTION => 'External routine error',
self::EXTERNAL_ROUTINE_INVOCATION_EXCEPTION => 'External routine invocation error',
self::SAVEPOINT_EXCEPTION => 'Savepoint error',
self::INVALID_CATALOG_NAME => 'Invalid catalog name',
self::INVALID_SCHEMA_NAME => 'Invalid schema name',
self::TRANSACTION_ROLLBACK => 'Transaction was rolled back',
self::SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION => 'Query syntax or permission error',
self::WITH_CHECK_OPTION_VIOLATION => 'Check option violation',
self::INSUFFICIENT_RESOURCES => 'Server resource limit reached',
self::PROGRAM_LIMIT_EXCEEDED => 'Program limit exceeded',
self::OBJECT_NOT_IN_PREREQUISITE_STATE => 'Object not in prerequisite state',
self::OPERATOR_INTERVENTION => 'Operator intervention',
self::SYSTEM_ERROR => 'Database system error',
self::SNAPSHOT_FAILURE => 'Snapshot failure',
self::CONFIGURATION_FILE_ERROR => 'Configuration file error',
self::FDW_ERROR => 'Foreign data wrapper error',
self::PL_PGSQL_ERROR => 'PL/pgSQL error',
self::INTERNAL_ERROR => 'Internal database error',
self::UNKNOWN => 'Database operation failed',
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,38 @@ final class QueryException extends ClientException
{
private const int SQL_PREVIEW_LENGTH = 100;

private ?string $sql = null;

public static function columnNotFound(string $column) : self
{
return new self(\sprintf('Column "%s" not found in result set', $column));
private function __construct(
string $message,
private readonly string $sql,
private readonly PostgreSqlError $error,
) {
parent::__construct($message);
}

public static function executionFailed(string $sql, string $error) : self
public static function executionFailed(string $sql, PostgreSqlError $error) : self
{
$sqlPreview = \strlen($sql) > self::SQL_PREVIEW_LENGTH
? \substr($sql, 0, self::SQL_PREVIEW_LENGTH) . '...'
: $sql;

$exception = new self(\sprintf('Query execution failed: %s. SQL: %s', $error, $sqlPreview));
$exception->sql = $sql;

return $exception;
}

public static function noRowsFound() : self
{
return new self('Expected exactly one row, but none were returned');
}

public static function sequenceNotUsed(string $sequenceName) : self
{
return new self(\sprintf('Sequence "%s" has not been used in this session', $sequenceName));
}

public static function tooManyRows(int $count) : self
{
return new self(\sprintf('Expected exactly one row, but %d were returned', $count));
return new self(
\sprintf(
'Query execution failed [%s]: %s. SQL: %s',
$error->sqlState,
$error->safeMessage(),
$sqlPreview
),
$sql,
$error
);
}

public static function unexpectedScalarType(string $expected, string $actual) : self
public function error() : PostgreSqlError
{
return new self(\sprintf('Expected scalar of type %s, got %s', $expected, $actual));
return $this->error;
}

/**
* Get the full SQL query that caused the exception.
*
* Note: This is only available for executionFailed exceptions.
* Use with caution - do not log or expose to users as it may contain sensitive data.
*/
public function sql() : ?string
public function sql() : string
{
return $this->sql;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Flow\PostgreSql\Client\Exception;

final class ResultException extends ClientException
{
public static function noRowsFound() : self
{
return new self('Expected exactly one row, but none were returned');
}

public static function sequenceNotUsed(string $sequenceName) : self
{
return new self(\sprintf('Sequence "%s" has not been used in this session', $sequenceName));
}

public static function tooManyRows(int $count) : self
{
return new self(\sprintf('Expected exactly one row, but %d were returned', $count));
}

public static function unexpectedScalarType(string $expected, string $actual) : self
{
return new self(\sprintf('Expected scalar of type %s, got %s', $expected, $actual));
}
}
Loading
Loading