pgfederate: ein PostgreSQL-Endpunkt für viele Datenquellen

Ein Gateway, das das PostgreSQL-Protokoll spricht und Joins in einem eingebetteten DuckDB ausführt, sodass Metabase und Grafana über ihren normalen PostgreSQL-Treiber auf PostgreSQL, MySQL, Dateien und REST-APIs zugreifen.

Das Problem

Ein BI-Werkzeug spricht meist mit genau einer Datenbank. Wünsche, das zu ändern, gehören im Tracker von Metabase zu den beliebtesten: Joins über mehrere Datenbanken haben 2110 Reaktionen und sind seit 2016 offen, ein REST-API-Treiber hat 889. Die Anleitung von Metabase nennt als Umwege ein Data Warehouse, Foreign Data Wrappers, Views und föderierte Query-Engines. Das funktioniert, verlangt aber einen Trino-Cluster oder einen PostgreSQL-Server, den man ändern darf, dazu Einrichtung pro Quelle.

Auch Projekte, die ein PostgreSQL-Protokoll vor DuckDB setzen, gibt es. PostHog hat duckgres im Oktober 2026 eingestellt, buenavista wurde seit Dezember 2024 nicht mehr geändert. Ich wollte den kleinen Fall abdecken: ein Prozess, den ein Team ohne Datenplattform neben Metabase startet.

Wie es arbeitet

pgfederate liest eine YAML-Datei. Jede Quelle wird zu einem Schema: PostgreSQL, MySQL und SQLite werden über die Erweiterungen von DuckDB angebunden, Dateien werden bei der Abfrage gelesen, und JSON-APIs werden nach Zeitplan geladen, mit Paginierung, Wiederholungen und dem Auflösen verschachtelter Objekte. Ein Eintrag unter relations erscheint bei Clients als Fremdschlüssel, sodass der Query Builder von Metabase Joins über Quellen hinweg ohne SQL anbietet.

Drei Teile machten die meiste Arbeit:

  • Der Server implementiert das PostgreSQL-Protokoll 3: SCRAM-Login, Prepared Statements, Portale, Abbruchanfragen und Ergebnisse im Text- und Binärformat.
  • Clients finden Tabellen über pg_catalog und information_schema. Die eigenen Views von DuckDB beschreiben DuckDB, deshalb führt pgfederate separate Tabellen, die eine Datenbank mit einem Schema pro Quelle zeigen, und baut sie nach jeder Aktualisierung neu.
  • Jede Anweisung wird vor der Ausführung mit sqlglot geparst. Nur Lesezugriffe auf freigegebene Tabellen kommen durch, DuckDB-Funktionen wie read_csv und query werden abgelehnt, und DuckDB selbst läuft mit gesperrtem externem Zugriff.

Beispiel

Der Schnellstart verbindet eine SQLite-Datenbank und eine CSV-Datei über psql:

pgfederate=> select c.name, sum(i.amount) as billed
             from shop.customers c join billing.invoices i on i.customer_id = c.id
             group by c.name order by billed desc;
  name  | billed
--------+--------
 Acme   |   30.5
 Globex |   5.25
(2 rows)

Das ist die Ausgabe des End-to-End-Laufs in der CI, in dem das echte Metabase über seinen PostgreSQL-Treiber mit dem Gateway spricht:

ok: metabase synced every source
ok: metabase sees the cross-source foreign key
ok: native query joins Postgres, MySQL and REST: [['Acme', 53.5, 2], ['Globex', 50.0, 1], ['Initech', 9.5, 0]]
ok: query builder join across MySQL and Postgres: {'Acme': 4, 'Globex': 2, 'Initech': 1}
ok: month bucketing: [['2026-03-01T00:00:00Z', 2], ['2026-04-01T00:00:00Z', 2]]
ok: filter with a bound parameter

Wie es getestet ist

Die Testsuite lässt psycopg im Text- und Binärmodus, asyncpg und psql gegen einen echten Server laufen und spielt die Katalogabfragen von Metabase, pgjdbc und Grafana ab, die ich aus deren Quellcode übernommen habe. Etwa 35 Tests versuchen Schreibzugriffe und Ausbrüche, zum Beispiel das direkte Lesen von Dateien oder angebundenen Datenbanken. PostgreSQL- und MySQL-Quellen laufen in der CI gegen echte Server, einschließlich einer Prüfung, dass Filter im Scan-Plan stehen. Ein zweiter CI-Job startet einen Compose-Stack und steuert Metabase und Grafana über ihre HTTP-APIs.

Der erste CI-Lauf fand keinen Fehler im Gateway, sondern nur einen in meiner Testabfrage, die Zeilen durch einen Join vervielfachte. Ein lokaler Lauf mit breiteren Abfragen fand dann einen echten: substring(x from 'muster') ist eine Regex-Suche, und Metabase bindet das Muster als Parameter. Rund 80 Fälle aus dem Query Builder laufen jetzt in der CI.

Grenzen

pgfederate ist nur lesend und läuft in einem Prozess, ersetzt also Trino bei großen Datenmengen nicht. Filter werden an PostgreSQL und MySQL weitergereicht, Aggregationen nicht, und ein Join über Quellen hinweg holt die benötigten Zeilen nach DuckDB. REST-Daten sind ein Stand der letzten Aktualisierung. SQL, das DuckDB nicht ausführen kann, schlägt fehl, auch wenn es gültiges PostgreSQL ist, und die Ausdrücke host und domain von Metabase scheitern, weil sie Regex-Lookaround nutzen. DECLARE und FETCH werden abgelehnt, benannte Cursor in psycopg funktionieren also nicht. Quoten pro Nutzer gibt es nicht. Getestet sind nur die oben genannten Clients.

Der Code liegt unter github.com/Jwrede/pgfederate.