Package · cdo

Error handling

Every database failure surfaces as a single exception type, CDOException, which always wraps the original PDOException. That means one catch for the whole DML surface, with the full SQLSTATE detail one getPrevious() away.

The exception

text
Flytachi\Winter\Cdo\Connection\CDOException
  └── extends \RuntimeException

CDOException is a thin RuntimeException subclass. It carries a human-readable message describing the operation, and its $previous is the underlying PDOException — preserving the SQLSTATE code, the driver’s message, and the stack trace.

When each method throws

Scenario Method
PDO connection failed CDO::__construct()
INSERT failed insert(), insertGroup()
UPDATE failed update()
DELETE failed delete()
Upsert query failed upsert(), upsertGroup()
conflictColumns is empty upsert(), upsertGroup()

in() / notIn() throw a different type

Passing an empty array to Qb::in() / Qb::notIn() throws InvalidArgumentException (not CDOException) — it’s a builder-time programming error, caught before any query runs. See Qb operators.

Catching failures

Catch CDOException, then reach into the previous PDOException for the code:

php
use Flytachi\Winter\Cdo\Connection\CDOException;

try {
  $cdo->insert('users', ['email' => 'alice@example.com', 'name' => 'Alice']);
} catch (CDOException $e) {
  echo $e->getMessage();                 // "Error when creating a record in the database (...)"

  $pdo = $e->getPrevious();              // the original PDOException
  echo $pdo?->getCode();                 // SQLSTATE, e.g. "23505"
  echo $pdo?->getMessage();              // driver message
}

Reacting to a specific error

Branch on the SQLSTATE code from the wrapped exception:

php
try {
  $cdo->insert('users', $data);
} catch (CDOException $e) {
  $pdo = $e->getPrevious();

  // 23xxx = integrity constraint (unique / FK) on both PG and MySQL
  if ($pdo && str_starts_with((string) $pdo->getCode(), '23')) {
      throw new DuplicateException('Already exists');
  }

  throw $e; // re-throw anything unrecognised
}

SQLSTATE reference

Common codes seen via $e->getPrevious()->getCode():

Code Meaning Database
23000 Integrity constraint violation MySQL / MariaDB
23505 Unique violation PostgreSQL
23503 Foreign key violation PostgreSQL
42P01 Undefined table PostgreSQL
42000 Syntax error MySQL / MariaDB
08006 Connection failure PostgreSQL
HY000 General error Various

Retry-on-failure pattern

For transient connection loss, reconnect once and retry:

php
try {
  $id = ConnectionPool::db(AppDb::class)->insert('events', $event);
} catch (CDOException $e) {
  ConnectionPool::getConfigDb(AppDb::class)->reconnect();
  $id = ConnectionPool::db(AppDb::class)->insert('events', $event);
}