Skip to content

How to use PostgreSQL ​

NENE2's PdoConnectionFactory supports the pgsql adapter out of the box. This guide covers the setup steps and PostgreSQL-specific patterns that differ from the MySQL and SQLite defaults.

Docker compose setup ​

Add a postgres service and set the environment variables for the app service:

yaml
# compose.yaml
services:
  app:
    environment:
      DB_ADAPTER: pgsql
      DB_HOST: db
      DB_PORT: "5432"
      DB_NAME: myapp
      DB_USER: myapp
      DB_PASSWORD: secret
      DB_CHARSET: utf8    # ignored for pgsql — see the charset section below
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp -d myapp"]
      interval: 5s
      timeout: 10s
      retries: 10

Dockerfile — pdo_pgsql extension ​

The pdo_pgsql extension is not enabled by default in the php:8.4-cli base image. Add it to your Dockerfile:

dockerfile
FROM php:8.4-cli

RUN apt-get update && apt-get install -y libpq-dev unzip postgresql-client \
    && docker-php-ext-install pdo pdo_pgsql \
    && rm -rf /var/lib/apt/lists/*

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html

Phinx migration configuration ​

Omit charset from the Phinx environment config when using pgsql — PostgreSQL does not use this key:

php
// phinx.php
$database->environment => [
    'adapter' => $database->adapter,   // 'pgsql'
    'host'    => $database->host,
    'name'    => $database->name,
    'user'    => $database->user,
    'pass'    => $database->password,
    'port'    => $database->port,
    // 'charset' => ...  ← omit for pgsql
],

Phinx maps its column types to PostgreSQL types automatically. Common mappings:

Phinx typePostgreSQL type
integer (primary key)SERIAL
stringVARCHAR(n)
textTEXT
datetimeTIMESTAMP(0) WITHOUT TIME ZONE
booleanBOOLEAN

Getting the ID after INSERT — use RETURNING id ​

DatabaseQueryExecutorInterface::lastInsertId() returns 0 on PostgreSQL because PDO::lastInsertId() requires a sequence name argument that NENE2 does not track.

Use fetchOne() with RETURNING id in your INSERT SQL to retrieve the generated primary key in a single round trip:

php
// ✓ PostgreSQL-compatible pattern
$row = $this->executor->fetchOne(
    'INSERT INTO reviews (book_title, content, rating, created_at) VALUES (?, ?, ?, ?) RETURNING id',
    [$bookTitle, $content, $rating, $now],
);
$id = (int) ($row['id'] ?? 0);

RETURNING id is standard PostgreSQL SQL and is also supported by SQLite ≥ 3.35 (2021). It is not supported by MySQL/MariaDB — keep your repository implementations DB-specific if you need to target both.

Why not lastInsertId()? ​

PostgreSQL sequences are named {table}_{column}_seq by default (e.g. reviews_id_seq). PDO::lastInsertId('reviews_id_seq') would work, but DatabaseQueryExecutorInterface does not expose a sequence-name parameter. RETURNING id is cleaner and avoids the coupling.

Resetting test data between tests ​

Test isolation requires resetting the table between each test case. The correct SQL differs by adapter:

php
protected function setUp(): void
{
    // PostgreSQL — resets the SERIAL sequence to 1
    $this->executor->execute('TRUNCATE reviews RESTART IDENTITY CASCADE');

    // MySQL — TRUNCATE resets AUTO_INCREMENT implicitly
    // $this->executor->execute('TRUNCATE reviews');

    // SQLite — TRUNCATE is not supported; use DELETE + sequence reset
    // $this->executor->execute('DELETE FROM reviews');
    // $this->executor->execute("DELETE FROM sqlite_sequence WHERE name = 'reviews'");
}

TRUNCATE … RESTART IDENTITY CASCADE is PostgreSQL-specific. RESTART IDENTITY resets the sequence so the next insert returns id = 1, keeping test assertions deterministic. CASCADE truncates dependent tables in the same statement.

Character encoding — DB_CHARSET is ignored for pgsql ​

The pgsql DSN does not include a charset parameter. DB_CHARSET / DatabaseConfig::charset is silently ignored for the pgsql adapter — only the mysql adapter uses it in the DSN.

PostgreSQL determines the client encoding from server_encoding at connection time. Databases created with the default locale use UTF-8, which is almost always what you want.

To force a specific encoding, use DATABASE_URL with the options parameter:

DATABASE_URL=pgsql://myapp:secret@db:5432/myapp?options=--client_encoding%3DUTF8

Or issue a SET command after the connection is established (not possible through PdoDatabaseQueryExecutor directly — you would need a custom connection factory).

Released under the MIT License.