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_catalogemulation)
- PostgreSQL protocol (via
- 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
- Intellij Datagrid
- Dbeaver
- psql cli
- Vscode Postgresql Extension
So testing with other clients, especially BI tools are very welcomed.
-
Better Python DX
-
Example apps (data lake, feature store, etc.)
Metadata
Release files for riffq 0.1.11
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| riffq-0.1.11.tar.gz | 126.1 kB | With provenance | Details |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| riffq-0.1.11-cp314-cp314-macosx_11_0_arm64.whl | CPython 3.14 | CPython 3.14 | macOS 11.0+ ARM64 | Details |
| riffq-0.1.11-cp312-cp312-win_amd64.whl | CPython 3.12 | CPython 3.12 | Windows x86-64 | Details |
| riffq-0.1.11-cp312-cp312-manylinux_2_38_x86_64.whl | CPython 3.12 | CPython 3.12 | Linux glibc 2.38+ x86-64 | Details |
Total release size: 169.1 MB
Release files / riffq-0.1.11.tar.gz
| Download URL | riffq-0.1.11.tar.gz |
|---|---|
| Size | 126.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f3660da2fb4bd051f949fda24a2cd3cda060a13edd0a1f7d036c5f1b6e894989
|
|
BLAKE2b-256 checksum How to use checksums |
d10995d8166a7e7efb749ede46ec2e81ac7b34addfaaa8f13a8b007350912426
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 22, 2026.
Transparency logRelease files / riffq-0.1.11-cp314-cp314-macosx_11_0_arm64.whl
| Download URL | riffq-0.1.11-cp314-cp314-macosx_11_0_arm64.whl |
|---|---|
| Size | 55.6 MB |
| Tags | CPython 3.14 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
d147943cefd5a33e33fad9461b2fe387268d68ec6271a5162a132ad32bca70e0
|
|
BLAKE2b-256 checksum How to use checksums |
9e61dc8d60a27110fd8b7acea3c43239c877e557c5323fef58958e8803fe3ed2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 22, 2026.
Transparency logRelease files / riffq-0.1.11-cp312-cp312-win_amd64.whl
| Download URL | riffq-0.1.11-cp312-cp312-win_amd64.whl |
|---|---|
| Size | 52.7 MB |
| Tags | CPython 3.12 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
3a51783747e1bfc246745f05b4d2e7b768c5c12df861c2f9cf390eeed7028d5a
|
|
BLAKE2b-256 checksum How to use checksums |
30b7cd2ad10ceb9b55bb93b6e1470f98f7f0f1aa070ffbd93205bb804f1da1c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 22, 2026.
Transparency logRelease files / riffq-0.1.11-cp312-cp312-manylinux_2_38_x86_64.whl
| Download URL | riffq-0.1.11-cp312-cp312-manylinux_2_38_x86_64.whl |
|---|---|
| Size | 60.6 MB |
| Tags | CPython 3.12 Linux glibc 2.38+ x86-64 |
|
SHA-256 checksum How to use checksums |
b1cda7cd7221addf5b9685fa1eb00c026fae72e5e5ef1c916b9f4c4c5436df6f
|
|
BLAKE2b-256 checksum How to use checksums |
346c03841d7a8d2cf2acf8f1fdb0728636a5703657a3057f33bf187d4520a6de
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 22, 2026.
Transparency logSecurity for riffq 0.1.11
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 4 files in this release have been uploaded with provenance metadata.
Attestation for riffq-0.1.11.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.