Skip to main content

riffq

riffq is a toolkit in python (built in Rust) for building PostgreSQL wire-compatible databases.
It allows you to serve data from Python over the PostgreSQL protocol — turning your Python-based logic or in-memory data into a queryable, network-exposed system. We also have a catalog emulation system in rust with datafusion.

Documentation: https://ybrs.github.io/riffq/


What It Does

  • Implements the PostgreSQL wire protocol in Rust for performance and concurrency
  • Sends raw SQL queries (Simple or Extended protocol) to Python for interpretation
  • Implements postgres catalog compatibility layer, see pg_catalog_rs

Since you are in python, you can

  • Allows you to connect to remote data sources (e.g., analytics DB, CRM) and expose them as a unified PostgreSQL database
  • Enables serving Pandas DataFrames over the network as virtual SQL tables
  • Can delegate SQL execution to DuckDB, Polars, or any other Python engine
  • Acts as a programmable federated query engine or custom data service

Example Use Cases

  • Serve a Pandas DataFrame as a PostgreSQL table to BI tools
  • Build a custom federated engine from multiple APIs or databases
  • Implement your own data lake query frontend
  • Expose dynamic ML feature stores for training or real-time inference
  • Provide fine-grained, code-controlled access to internal metrics or logs

Example

import logging
import duckdb
import pyarrow as pa
import riffq
logging.basicConfig(level=logging.DEBUG)

class Connection(riffq.BaseConnection):
    def handle_auth(self, user, password, host, database=None, callback=callable):
        # simple username/password check
        callback(user == "user" and password == "secret")

    def handle_connect(self, ip, port, callback=callable):
        # allow every incoming connection
        callback(True)

    def handle_disconnect(self, ip, port, callback=callable):
        # invoked when client disconnects
        callback(True)

    def _handle_query(self, sql, callback, **kwargs):
        cur = duckdb_con.cursor()
        try:
            if sql.strip().lower() == "select err":
                # custom error returned to client
                callback(("ERROR", "42846", "bad type"), is_error=True)
                return
            reader = cur.execute(sql).fetch_record_batch()
            self.send_reader(reader, callback)
        except Exception as exc:
            logging.exception("error on executing query")
            batch = self.arrow_batch(
                [pa.array(["ERROR"]), pa.array([str(exc)])],
                ["error", "message"],
            )
            self.send_reader(batch, callback)

    def handle_query(self, sql, callback=callable, **kwargs):
        self.executor.submit(self._handle_query, sql, callback, **kwargs)

def main():
    global duckdb_con
    duckdb_con = duckdb.connect()
    duckdb_con.execute(
        """
        CREATE VIEW klines AS 
        SELECT * 
        FROM 'data/klines.parquet'
        """
    )
    server = riffq.RiffqServer("127.0.0.1:5433", connection_cls=Connection)
    server.set_tls("certs/server.crt", "certs/server.key")
    server.start(tls=True)

if __name__ == "__main__":
    main()

The Rust side calls this Python handler when a SQL query comes in via the PostgreSQL protocol.


Architecture

  • Rust layer handles:
    • PostgreSQL protocol (via pgwire)
    • Connection management
    • Query routing
    • Metadata compatibility (pg_catalog emulation)
  • Python layer handles:
    • SQL execution (via any engine: DuckDB, Polars, etc.)
    • Data transformation
    • Custom logic and dynamic schema definitions

Zero Copy

We try to achieve zero-copy by using arrow/pycapsule. So data from duckdb comes to python as a pycapsule pointer, which goes to thread in python which goes to the callback in rust still as a pycapsule pointer. We then stream to network with postgresql using pgwire.

https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html


Getting Started

Install the package

pip install riffq --pre

Extend BaseConnection class

You can extend the base class for

  • handle_query
  • handle_auth - To check for username/password
  • handle_connect - To check ip/port restriction
  • handle_disconnect - WIP
class Connection(riffq.BaseConnection):
    def handle_auth(self, user, password, host, database=None, callback=callable):
        # simple username/password check
        callback(user == "user" and password == "secret")

    def handle_connect(self, ip, port, callback=callable):
        # allow every incoming connection
        callback(True)

    def handle_disconnect(self, ip, port, callback=callable):
        # invoked when client disconnects
        callback(True)

    def _handle_query(self, sql, callback, **kwargs):
        cur = duckdb_con.cursor()
        try:
            if sql.strip().lower() == "select err":
                # custom error returned to client
                callback(("ERROR", "42846", "bad type"), is_error=True)
                return
            reader = cur.execute(sql).fetch_record_batch()
            self.send_reader(reader, callback)
        except Exception as exc:
            logging.exception("error on executing query")
            batch = self.arrow_batch(
                [pa.array(["ERROR"]), pa.array([str(exc)])],
                ["error", "message"],
            )
            self.send_reader(batch, callback)

    def handle_query(self, sql, callback=callable, **kwargs):
        self.executor.submit(self._handle_query, sql, callback, **kwargs)

Start the server

    global duckdb_con
    duckdb_con = duckdb.connect()
    duckdb_con.execute(
        """
        CREATE VIEW klines AS 
        SELECT * 
        FROM 'data/klines.parquet'
        """
    )
    server = riffq.RiffqServer("127.0.0.1:5433", connection_cls=Connection)
    server.set_tls("certs/server.crt", "certs/server.key")
    server.start()

You can check server implementations on test_concurrency/ and example/ directory

Then connect using any PostgreSQL client:

psql -h localhost -p 5433

Enabling TLS

Generate a temporary certificate and key:

openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 1 -out server.crt -subj "/CN=localhost"

Enabling Catalog Emulation

Postgresql clients sends queries to pg_catalog schema to find out databases, schemas, tables, columns.

We have this pg_catalog_rs for this purpose.

You can register your own database and your tables.

For example

    server = riffq.RiffqServer(f"127.0.0.1:{port}", connection_cls=Connection)
    server.set_tls("certs/server.crt", "certs/server.key")

    server._server.register_database("duckdb")

    tbls = duckdb_con.execute(
        "SELECT table_schema, table_name FROM information_schema.tables "
        "WHERE table_schema NOT IN ('pg_catalog','information_schema')"
    ).fetchall()

    for schema_name, table_name in tbls:
        server._server.register_schema("duckdb", schema_name)
        cols_info = duckdb_con.execute(
            "SELECT column_name, data_type, is_nullable FROM information_schema.columns "
            "WHERE table_schema=? AND table_name=?",
            (schema_name, table_name),
        ).fetchall()
        columns = []
        for col_name, data_type, is_nullable in cols_info:
            columns.append(
                {
                    col_name: {
                        "type": map_type(data_type),
                        "nullable": is_nullable.upper() == "YES",
                    }
                }
            )
        server._server.register_table("duckdb", schema_name, table_name, columns)

    server.start(catalog_emulation=True)

Lazy (callback-driven) catalog

The register_* calls above snapshot the catalog at startup. For a live source, install a lazy catalog instead: supply one source object and Riffq pulls catalog metadata from it on every pg_catalog scan, so tables created after startup show up automatically — nothing is cached.

class MyCatalog:
    def databases(self, callback):
        callback([{"oid": 16384, "name": "appdb"}])
    def schemas(self, database, callback):
        callback([{"oid": 16385, "name": "public"}])
    def relations(self, database, schema, callback):
        callback([{"oid": 20001, "reltype_oid": 30001, "name": "users",
                   "kind": "table", "has_index": False}])
    def columns(self, database, schema, relation, callback):
        callback([{"name": "id", "type_oid": 23, "nullable": False}])  # 23 = int4

server.set_lazy_catalog(MyCatalog())   # replaces the eager register_* calls
server.start(catalog_emulation=True)

You own the OIDs (stable + unique); type_oid is a pg_type OID. See docs/catalog.md for the full contract and example/lazy_catalog.py for a runnable example; Teleduck uses this path against a live DuckDB connection.


Status

  • ✅ Wire protocol support (simple + extended)
  • ✅ Query dispatching to Python
  • ✅ Thread based non-blocking query execution (long queries don't block)
  • ✅ DuckDB, Pandas, Polars compatibility
  • ✅ Limited SQL parsing on Rust side (forwarded to Python)
  • ✅ Optional TLS encryption
  • ✅ Integration with optional catalog emulation layer
  • 🟡 More examples
  • 🟡 Better logging, monitoring, observability

Installation

We currently have a pre release on pypi. You can install it with --pre tag.

pip install riffq --pre

Running Locally

Install the development requirements and run the test suite:

git clone git@github.com:ybrs/riffq.git
cd riffq
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

maturin build --profile=fast -i python3
pip install target/wheels/*.whl
# or maturin developer
make all-tests

The tests require the Rust extension to build successfully; any build failure will cause the suite to fail.


License

MIT or Apache 2.0 — your choice.


Contributing

Contributions are welcome! Especially for:

  • For the emulation layer, I am currently testing with

    So testing with other clients, especially BI tools are very welcomed.

  • Better Python DX

  • Example apps (data lake, feature store, etc.)


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

riffq-0.1.11.tar.gz (126.1 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

riffq-0.1.11-cp314-cp314-macosx_11_0_arm64.whl (55.6 MB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

riffq-0.1.11-cp312-cp312-win_amd64.whl (52.7 MB view details)

Uploaded CPython 3.12Windows x86-64

riffq-0.1.11-cp312-cp312-manylinux_2_38_x86_64.whl (60.6 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.38+ x86-64

File details

Details for the file riffq-0.1.11.tar.gz.

File metadata

  • Download URL: riffq-0.1.11.tar.gz
  • Upload date:
  • Size: 126.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for riffq-0.1.11.tar.gz
Algorithm Hash digest
SHA256 f3660da2fb4bd051f949fda24a2cd3cda060a13edd0a1f7d036c5f1b6e894989
MD5 572cf6f9fec1985d3041710f743cd513
BLAKE2b-256 d10995d8166a7e7efb749ede46ec2e81ac7b34addfaaa8f13a8b007350912426

See more details on using hashes here.

Provenance

The following attestation bundles were made for riffq-0.1.11.tar.gz:

Publisher: build-release.yml on ybrs/riffq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file riffq-0.1.11-cp314-cp314-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for riffq-0.1.11-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d147943cefd5a33e33fad9461b2fe387268d68ec6271a5162a132ad32bca70e0
MD5 b67ed4969c111e28f0f0399da41c1614
BLAKE2b-256 9e61dc8d60a27110fd8b7acea3c43239c877e557c5323fef58958e8803fe3ed2

See more details on using hashes here.

Provenance

The following attestation bundles were made for riffq-0.1.11-cp314-cp314-macosx_11_0_arm64.whl:

Publisher: build-release.yml on ybrs/riffq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file riffq-0.1.11-cp312-cp312-win_amd64.whl.

File metadata

  • Download URL: riffq-0.1.11-cp312-cp312-win_amd64.whl
  • Upload date:
  • Size: 52.7 MB
  • Tags: CPython 3.12, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for riffq-0.1.11-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 3a51783747e1bfc246745f05b4d2e7b768c5c12df861c2f9cf390eeed7028d5a
MD5 c6627d0af5105e065ba470afb7e258ba
BLAKE2b-256 30b7cd2ad10ceb9b55bb93b6e1470f98f7f0f1aa070ffbd93205bb804f1da1c7

See more details on using hashes here.

Provenance

The following attestation bundles were made for riffq-0.1.11-cp312-cp312-win_amd64.whl:

Publisher: build-release.yml on ybrs/riffq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file riffq-0.1.11-cp312-cp312-manylinux_2_38_x86_64.whl.

File metadata

File hashes

Hashes for riffq-0.1.11-cp312-cp312-manylinux_2_38_x86_64.whl
Algorithm Hash digest
SHA256 b1cda7cd7221addf5b9685fa1eb00c026fae72e5e5ef1c916b9f4c4c5436df6f
MD5 1ed33abfbb0d931c9818bb95e13685e7
BLAKE2b-256 346c03841d7a8d2cf2acf8f1fdb0728636a5703657a3057f33bf187d4520a6de

See more details on using hashes here.

Provenance

The following attestation bundles were made for riffq-0.1.11-cp312-cp312-manylinux_2_38_x86_64.whl:

Publisher: build-release.yml on ybrs/riffq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.11 This release

4 files

0.1.7

4 files

0.1.6

4 files

0.1.3

4 files

0.1.0

4 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page