PHP DuckDB Extension

September 10, 2025 · View on GitHub

In‑process analytics for PHP powered by DuckDB.
This extension delivers fast on‑disk and in‑memory SQL, prepared statements, streaming fetch, bulk appends, and — most importantly — first‑class PHP User‑Defined Functions (UDFs) so you can call your own PHP code directly from SQL.

Highlights

  • PHP UDFs: Register PHP callables and use them inside SQL.
  • Prepared statements (positional parameters).
  • Streaming result iteration over millions of rows.
  • Bulk inserts via Appender (with flush() for immediate visibility).
  • Flexible fetching (objects by default, associative arrays optional).
  • Clear exceptions via \Fnvoid\DuckDB\Exception.

Table of Contents


Installation

DuckDB PHP Extension Installation Guide

This guide explains how to install DuckDB and compile the PHP extension on Linux. Pre-built binaries will be provided in the future for easier installation.

⚠️ This extension is supported on PHP 8.0 and above.


1. Install Necessary Build Tools

Ubuntu / Debian
sudo apt update
sudo apt install -y php-dev build-essential git wget unzip
RedHat / Fedora / CentOS / AlmaLinux
sudo dnf update -y
sudo dnf install -y php-devel gcc make git wget unzip

2. Install DuckDB

x86_64
wget https://github.com/duckdb/duckdb/releases/download/v1.3.2/libduckdb-linux-amd64.zip
sudo unzip libduckdb-linux-amd64.zip duckdb.h -d /usr/local/include/
sudo unzip libduckdb-linux-amd64.zip libduckdb.so -d /usr/local/lib/
rm libduckdb-linux-amd64.zip
sudo ldconfig
arm64
wget https://github.com/duckdb/duckdb/releases/download/v1.3.2/libduckdb-linux-arm64.zip
sudo unzip libduckdb-linux-arm64.zip duckdb.h -d /usr/local/include/
sudo unzip libduckdb-linux-arm64.zip libduckdb.so -d /usr/local/lib/
rm libduckdb-linux-arm64.zip
sudo ldconfig

⚠️ Make sure the architecture of the DuckDB binary matches your system.


3. Compile the PHP Extension

git clone https://github.com/fnvoid64/php-duckdb.git
cd php-duckdb
phpize
./configure
make
make test
sudo make install

After installation, enable the extension in your php.ini:

extension=duckdb.so

4. Future Plans

Pre-built binaries for popular Linux distributions and architectures will be published soon, making installation much simpler without compiling from source.


Creating a Connection

In‑memory

try {
    $db = new \Fnvoid\DuckDB\DuckDB(); // In‑memory database
} catch (\Fnvoid\DuckDB\Exception $e) {
    echo $e->getMessage();
}

File‑backed

try {
    $db = new \Fnvoid\DuckDB\DuckDB('/path/to/db');
} catch (\Fnvoid\DuckDB\Exception $e) {
    echo $e->getMessage();
}

With Configuration

You can pass DuckDB configuration either as the second argument or via a named array $config.

try {
    $db = new \Fnvoid\DuckDB\DuckDB('/path/to/db', [
        'threads' => 8,
    ]);

    // or using a named argument:
    $db = new \Fnvoid\DuckDB\DuckDB(config: [
        'threads' => 8,
    ]);
} catch (\Fnvoid\DuckDB\Exception $e) {
    echo $e->getMessage();
}

Query Execution

\Fnvoid\DuckDB\DuckDB::query() returns a \Fnvoid\DuckDB\Result object that you can fetch from.

try {
    $db = new \Fnvoid\DuckDB\DuckDB(); // In‑memory
    $res = $db->query("CREATE TABLE test_table (id INT, name VARCHAR, age INT)");
} catch (\Fnvoid\DuckDB\Exception $e) {
    echo $e->getMessage();
}

Prepared Statements

Prepared statements currently support positional parameters only.
\Fnvoid\DuckDB\DuckDB::prepare() returns a \Fnvoid\DuckDB\Statement.

try {
    $db = new \Fnvoid\DuckDB\DuckDB();
    $stmt = $db->prepare("SELECT * FROM test_table WHERE id = ?");
    $res  = $stmt->execute([1]);
    $row  = $res->fetchOne(); // Fetch a single row
} catch (\Fnvoid\DuckDB\Exception $e) {
    echo $e->getMessage();
}

Fetching Data

Efficient, lazy iteration suitable for very large result sets.

try {
    $db = new \Fnvoid\DuckDB\DuckDB();
    $stmt = $db->prepare("SELECT * FROM test_table WHERE status = ?");
    $res  = $stmt->execute([1]);

    foreach ($res->iterate() as $row) {
        // Process row
    }
} catch (\Fnvoid\DuckDB\Exception $e) {
    echo $e->getMessage();
}

2) Get everything with fetchAll()

Best for small result sets.

$res = $stmt->execute([1])->fetchAll();

3) Get a single row with fetchOne()

$res = $stmt->execute([1])->fetchOne();

Fetch modes

All fetchers (iterate(), fetchAll(), fetchOne()) accept an optional boolean $assoc:

  • false (default) → objects
  • trueassociative arrays

Appender

Use the Appender for bulk inserts. Values are positional and must match the table schema order.
Data is not immediately visible to queries until you call flush().

try {
    $db = new \Fnvoid\DuckDB\DuckDB();
    $appender = $db->createAppender('table_name');

    foreach ($someLocalData as $row) {
        $appender->appendRow([
            $row->id,
            $row->name,
            $row->age,
        ]);
    }

    // Make appended data visible to queries
    $appender->flush();

    $res = $db->query("SELECT * FROM table_name");
} catch (\Fnvoid\DuckDB\Exception $e) {
    echo $e->getMessage();
}

User Defined Functions (UDFs)

Run your own PHP code inside DuckDB SQL. This is a major feature for many workloads (custom transforms, business rules, domain logic, etc.).

Requirements & Notes

  • UDFs require single‑threaded execution: set threads = 1 in config.
  • Supported return types: string, int, float, double, null, bool.
  • Register PHP functions by name; then call them from SQL.
try {
    $db = new \Fnvoid\DuckDB\DuckDB(config: [
        'threads' => 1, // Required for UDFs
    ]);

    // Register a PHP function
    $db->registerFunction('php_upper', fn (string $s): string => strtoupper($s));

    $stmt = $db->prepare("
        SELECT id, name, php_upper(name) AS name_upper
        FROM test_table
        WHERE status = ?
    ");

    $row = $stmt->execute([1])->fetchOne();
} catch (\Fnvoid\DuckDB\Exception $e) {
    echo $e->getMessage();
}

Errors & Exceptions

All failures throw \Fnvoid\DuckDB\Exception. Wrap calls in try/catch to handle errors gracefully.

try {
    $db = new \Fnvoid\DuckDB\DuckDB('/invalid/path');
} catch (\Fnvoid\DuckDB\Exception $e) {
    // Log / rethrow / surface message
    error_log($e->getMessage());
}

License

MIT License

© 2025 Pranto

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.