Provisa-pgwire-Server¶
Provisa stellt einen Endpunkt für das PostgreSQL-Wire-Protokoll (pgwire) bereit. Jedes Tool, das das PostgreSQL-Client-Protokoll spricht — psycopg2, asyncpg, DBeaver, Tableau, JDBC —, kann sich verbinden und Provisa-Daten über dieselbe Governance-Pipeline abfragen, die auch die HTTP-API steuert. (REQ-266)
Abfragen durchlaufen den vollständigen Governance-Stack: Durchsetzung der Sicherheit auf Zeilenebene, Maskierungsregeln, Beziehungsschutz, Domänenzugriffsprüfungen. (REQ-001, REQ-002, REQ-263) Die pgwire-Schnittstelle ist kein Umgehungsweg. (REQ-002, REQ-266)
Verbindungsdetails¶
Der Server startet, wenn PROVISA_PGWIRE_PORT auf eine Ganzzahl ungleich null gesetzt ist. Standardmäßig ist er deaktiviert. (REQ-527) [tool-verified: app.py:1739]
TLS. Setzen Sie PROVISA_PGWIRE_CERT und PROVISA_PGWIRE_KEY auf die Pfade eines PEM-Zertifikats und -Schlüssels. Sind beide vorhanden, verpackt der Server eingehende Verbindungen in TLS. Fehlen sie, ist TLS deaktiviert, und der Server antwortet auf SSL-Verhandlungsanfragen mit N. (REQ-530) [tool-verified: server.py:1746-1750]
Gemeldete Serverversion. Clients sehen 14.0.provisa. Tools, die Funktionen anhand der Versionsnummer freischalten, verhalten sich möglicherweise so, als wären sie mit PostgreSQL 14 verbunden. (REQ-579) [tool-verified: server.py:208]
Authentifizierung¶
Das Startup-Paket führt einen Benutzernamen und ein einzelnes Geheimnisfeld mit, aber kein Schema, das angibt, was dieses Geheimnis ist. Provisa entscheidet anhand des Geheimnisses selbst, sodass ein Client außer user und password keine weitere Konfiguration benötigt:
| Das Geheimnis ist | Erkannt an | Wird aufgelöst zu |
|---|---|---|
| Ein Personal Access Token | seinem Präfix provisa_pat_ |
Eigentümer und Rolle des Tokens (REQ-1263) |
| Ein OIDC-/Provider-Bearer-Token | daran, dass der konfigurierte Provider ein Token-Provider ist | die Identität, die das Token behauptet (REQ-890) |
| Ein Passwort | allem anderen | das Konto im konfigurierten Provider (basic oder simple) |
Die Entscheidung fällt einmal. Ein Credential, das der gewählte Validator ablehnt, wird nicht erneut gegen einen anderen geprüft — aus einer Ablehnung wird also kein zweiter Versuch.
Der Trust-Modus (provider: none oder inaktive Auth-Middleware) ist die Ausnahme: Der Benutzername wird direkt als role_id verwendet und das Geheimnis wird ignoriert. Verwenden Sie ihn nicht über eine unverschlüsselte Verbindung.
SCRAM-SHA-256. Unter provider: basic mit auth.scram: true kündigt der Server SASL (Authentifizierungscode 10) mit SCRAM-SHA-256 an, und das Passwort wird bewiesen statt gesendet. (REQ-1394) SCRAM-SHA-256-PLUS wird nicht angeboten. Einem Benutzer, für den noch kein Verifier geschrieben wurde — Verifier lassen sich nicht aus bcrypt-Hashes ableiten —, antwortet der Server mit einem Schein-Austausch, damit auf der Leitung nicht erkennbar wird, wer bereits migriert ist; dieser Benutzer authentifiziert sich per Klartextpasswort über TLS, bis seine nächste Passworteingabe einen Verifier schreibt. Ist auth.scram aus, verwendet der Server den PG-Auth-Typ 3 (Klartextpasswort). MD5 wird in keinem der beiden Fälle unterstützt.
Client-Zertifikate. Setzen Sie PROVISA_MTLS_CLIENT_CA, und der Server prüft während des Handshakes ein Client-Zertifikat, bevor irgendein Credential betrachtet wird. (REQ-1228) Mit PROVISA_MTLS_BIND_PRINCIPAL muss der Common Name des Zertifikats dem user entsprechen, als den sich die Verbindung anschließend authentifiziert. Siehe Konfiguration.
Fehlversuche werden gezählt. Fünf Fehlversuche in fünf Minuten sperren das Konto für fünfzehn Minuten, und der Zähler wird mit HTTP und Bolt geteilt — eine auf einer Oberfläche erwirkte Sperre gilt auf allen. (REQ-1393)
Auswahl einer Organisation. In einem Multi-Org-Deployment verbinden Sie sich mit <org>.<ihre-domain>, und pgwire liest die Organisation aus dem Hostnamen im TLS-ClientHello — so, wie HTTP sie aus dem Host-Header liest. (REQ-1234) Der Hostname fordert eine Organisation an; er gewährt sie nicht, und ein Principal ohne Mitgliedschaft dort wird abgewiesen. Eine Verbindung über eine IP-Adresse fordert keine Organisation an.
Was funktioniert¶
SELECT¶
Alle SELECT-Anweisungen durchlaufen die Governance-Pipeline (_pipeline.py). (REQ-001, REQ-262, REQ-266) Die Pipeline:
- Schreibt semantisches SQL in physisches SQL um (
rewrite_semantic_to_physical) - Wendet Governance an (Sicherheit auf Zeilenebene, Maskierung, Domänenzugriff) (REQ-263)
- Validiert gegen das registrierte Schema (REQ-011)
- Routet zu Trino oder direkt zum Quellen-Pool (REQ-027, REQ-028)
Einfache Abfragen mit mehreren Anweisungen werden unterstützt. Durch Semikolon getrennte Anweisungen werden aufgeteilt und der Reihe nach ausgeführt. (REQ-580) [tool-verified: server.py:318-381]
Parametrisierte Abfragen ($1, $2, ...) werden sowohl im Simple-Query- als auch im Extended-Query-Modus (Bind/Execute) unterstützt. Parameter werden vor der Ausführung als Literale eingesetzt. (REQ-581) [tool-verified: server.py:78-85]
SELECT * FROM fn(args) und SELECT fn(args) — wobei fn eine registrierte, nachverfolgte Funktion benennt — werden vor der Governance-Pipeline abgefangen und über den einen governance-gesteuerten Executor (invoke_tracked_function) geroutet. Das Ergebnis ist eine typisierte Zeilenmenge, identisch zu dem, was jede andere Oberfläche für diesen Befehl liefert. writable_by und die Governance-Regeln werden innerhalb des Executors durchgesetzt. (REQ-1156) [tool-verified: provisa/pgwire/function_call.py:74-88]
DDL¶
DDL-Anweisungen werden über den regulären Ausdruck in server.py erkannt und an DdlHandler weitergeleitet. Die Rolle muss die Capability "ddl" besitzen. (REQ-042) Ohne sie wird die Anweisung mit SQLSTATE 42501 abgelehnt. [tool-verified: ddl_handler.py:82-83]
Die erkannten DDL-Formen sind:
CREATE TABLE / VIEW / INDEX / UNIQUE INDEX / SEQUENCE / SCHEMA
ALTER TABLE / INDEX / SEQUENCE / VIEW
DROP TABLE / VIEW / INDEX / SEQUENCE / SCHEMA
[tool-verified: server.py:56-61]
Abhängig von ddl_catalog gibt es zwei Ausführungspfade: (REQ-582)
Trino-Pfad — wird verwendet, wenn ddl_catalog ein Iceberg-, Hive- oder anderer nicht registrierter Trino-Katalog ist (z. B. iceberg, hive, otel, results). Auf diesem Pfad werden nur CREATE TABLE und CREATE VIEW unterstützt. Der Versuch von ALTER, DROP oder CREATE INDEX löst einen Fehler aus. Der Tabellenname wird vollständig qualifiziert als catalog.schema.table. [tool-verified: ddl_handler.py:92-100]
Direkter Pfad — wird verwendet, wenn ddl_catalog einer registrierten Quellen-ID entspricht. Vollständiges DDL wird unterstützt: CREATE, ALTER, DROP, Indizes, Sequenzen. CREATE TABLE und CREATE VIEW werden schemaqualifiziert als schema.table. Alles andere DDL (ALTER, DROP, CREATE INDEX) wird nach dem Setzen des Schemakontexts unverändert durchgereicht. Bei PostgreSQL- und SQLite-Quellen wird der Kontext mit SET search_path TO schema gesetzt. Bei MySQL und MariaDB mit USE schema. [tool-verified: ddl_handler.py:139-170, ddl_handler.py:207-213]
Nach dem DDL wird auf beiden Pfaden die neue Tabelle in den Kompilierungskontext der Rolle aufgenommen, sodass sie sofort abfragbar ist. (REQ-583) [tool-verified: ddl_handler.py:216-250]
Auflösung des Schreibziels. Katalog und Schema für DDL stammen aus den Feldern ddl_catalog und ddl_schema der Domäne. Ist ddl_catalog nicht gesetzt, verwendet das System standardmäßig den Iceberg-Katalog. Ist ddl_schema nicht gesetzt, wird standardmäßig die Domänen-ID verwendet. Die Domäne wird über die domain_access-Liste der Rolle aufgelöst. (REQ-584) [tool-verified: app.py:804-811, ddl_handler.py:104-115]
COPY¶
COPY ... TO STDOUT und COPY ... FROM STDIN werden beide unterstützt. (REQ-585) [tool-verified: copy_handler.py:231-257]
COPY TO STDOUT — exportiert Abfrageergebnisse im PG-COPY-Wire-Format. Zwei Formen funktionieren:
-- Table reference
COPY my_table TO STDOUT WITH (FORMAT csv)
-- Arbitrary query
COPY (SELECT col1, col2 FROM my_table WHERE ...) TO STDOUT WITH (FORMAT text)
Unterstützte Formate: text (tabulatorgetrennt, Standard) und csv. Das Binärformat wird bei der COPY-Ausgabe nicht unterstützt. [tool-verified: copy_handler.py:36-52]
COPY FROM STDIN — fügt Zeilen in eine Zieltabelle ein. Beschränkt auf Quellen der Typen postgresql, mysql, sqlite oder mariadb. (REQ-586) Der Versuch von COPY FROM gegen eine reine Trino-Quelle (z. B. Iceberg) löst einen Berechtigungsfehler aus. [tool-verified: copy_handler.py:65, copy_handler.py:351-356]
Wird keine Spaltenliste angegeben, werden die Spalten aus dem registrierten Schema abgeleitet. [tool-verified: copy_handler.py:357]
Transaktionen und Sitzungsbefehle¶
SET, BEGIN, COMMIT, ROLLBACK, SAVEPOINT, RELEASE, DISCARD, RESET und DEALLOCATE werden abgefangen und liefern eine leere Erfolgsantwort. (REQ-587) Der Server ist hinsichtlich Transaktionen zustandslos — es gibt weder Transaktionsisolation noch Rollback-Unterstützung. (REQ-587) [tool-verified: catalog.py:27-31, catalog.py:1129-1132]
Katalog-Interception¶
Abfragen gegen information_schema und pg_catalog werden lokal beantwortet, ohne einen Trino-Roundtrip. (REQ-532) Die Interception-Schicht baut pro Anfrage eine In-Memory-DuckDB-Datenbank auf, die aus dem Kompilierungskontext der Rolle befüllt wird. (REQ-532) [tool-verified: catalog.py:210-213]
Abgefangene Tabellen:
information_schema: schemata, tables, columns, views, table_constraints, key_column_usage, referential_constraints
pg_catalog: pg_namespace, pg_class, pg_attribute, pg_type, pg_attrdef, pg_description, pg_index, pg_constraint, pg_proc, pg_roles, pg_auth_members, pg_database, pg_settings, pg_tables, pg_stat_user_tables, pg_statio_user_tables, pg_am, pg_extension, pg_enum, pg_stat_activity
[tool-verified: catalog.py:39-67]
pg_constraint wird mit echten PK- und FK-Daten befüllt, die aus den Feldern pk_columns und joins des Domänenmodells abgeleitet werden. (REQ-392, REQ-399) BI-Tools, die Fremdschlüsselbeziehungen untersuchen (Tableau, DBeaver usw.), sehen den Join-Graphen, den Provisa kennt. [tool-verified: catalog.py:551-632] Einspaltige Joins zwischen demselben Quelle/Ziel-Paar, deren Zielspalten zusammen den zusammengesetzten Primärschlüssel des Ziels bilden, werden zu einer einzigen FK-Zeile mit mehrelementigen conkey/confkey-Arrays zusammengefasst. (REQ-1094) [tool-verified: catalog_constraints.py]
pg_index wird mit einer Zeile pro Primärschlüssel- und UNIQUE-Constraint befüllt (indrelid = OID der Tabelle, indkey = geordnete Schlüssel-Attnums, indisprimary/indisunique gesetzt). Clients, die Schlüsselspalten über pg_index.indkey statt über pg_constraint auflösen — zum Beispiel DataGrip —, ermitteln die korrekten Spalten über den Standard-Join pg_index → pg_attribute. (REQ-1095) [tool-verified: catalog_constraints.py:340-384]
Zusätzlich werden folgende skalare Ausdrücke abgefangen: (REQ-588)
current_user,session_user→ die authentifizierterole_idcurrent_database()→"provisa"current_schema()→"public"version()→"PostgreSQL 14.0 on Provisa"pg_backend_pid()→0current_setting(...)→ liefert einen Wert aus einer festen EinstellungstabelleSHOW <setting>→ liefert einen Wert aus derselben Einstellungstabelle
[tool-verified: catalog.py:168-207, catalog.py:1076-1120]
Binäre Parameterkodierung¶
Das Extended-Query-Protokoll (Bind/Execute) unterstützt binär kodierte Parameter. (REQ-589) Die folgenden Typ-OIDs werden aus dem Binärformat dekodiert: [tool-verified: postgres.py:69-97]
| OID | PG-Typ | Python-Typ |
|---|---|---|
| 16 | bool | bool |
| 17 | bytea | bytes |
| 20 | int8 | int |
| 21 | int2 | int |
| 23 | int4 | int |
| 25 | text | str |
| 700 | float4 | float |
| 701 | float8 | float |
| 1043 | varchar | str |
| 1082 | date | datetime.date |
| 1114 | timestamp | datetime.datetime |
| 1184 | timestamptz | datetime.datetime (UTC) |
| 1700 | numeric | decimal.Decimal |
| 2950 | uuid | str |
Jede OID, die nicht in dieser Tabelle steht, löst "Unsupported binary parameter type: <oid>" aus. (REQ-589) [tool-verified: postgres.py:579]
Ergebnisspalten werden ebenfalls binär gesendet, wenn der Client dies anfordert, für denselben Typsatz plus ARRAY, JSON, INTERVAL und BIGINT. (REQ-589) [tool-verified: postgres.py:191-244]
Treiberempfehlungen¶
Native Python-Treiber (psycopg2, asyncpg). Diese handeln standardmäßig das Extended-Query-Protokoll aus und verwenden für die meisten Typen binäre Kodierung. Die Typtreue ist hier am höchsten — NUMERIC-Spalten kommen als Decimal, TIMESTAMP als datetime an, und so weiter. Verwenden Sie diese für Python-basierte ETL, Skripte oder direkte Integration.
JDBC (PostgreSQL-JDBC-Treiber). Verwenden Sie diesen für Java-Ökosystem-Tools: DBeaver, Tableau, Power BI, Metabase, JDBC-Operatoren von Airflow. JDBC verwendet standardmäßig das Simple-Query-Protokoll, was Komplikationen durch Binärkodierung vermeidet. Verbindungszeichenfolge:
Manche JDBC-basierten BI-Tools senden beim Verbindungsaufbau eine Reihe von Abfragen an information_schema und pg_catalog, um ihren Schema-Browser zu befüllen. Diese werden alle von der Katalog-Interception-Schicht beantwortet — bei der Schemaprüfung entsteht kein Trino-Verkehr. (REQ-532)
Wann welcher Treiber vorzuziehen ist. Ist der Client in Python geschrieben, verwenden Sie psycopg2 oder asyncpg für eine bessere Typbehandlung. Ist der Client ein BI-Tool oder eine beliebige JVM-Anwendung, verwenden Sie JDBC. Vermeiden Sie es, binäre und textbasierte Protokollerwartungen in derselben Verbindung zu mischen, wenn Sie überraschende Typkonvertierungen beobachten — das Textmodus-Verhalten von JDBC ist einfacher nachzuvollziehen.
Einschränkungen und Randbedingungen¶
Nur SQL; keine DML-Mutationen. Der pgwire-Listener analysiert und führt ausschließlich SQL aus — GraphQL- und Cypher-Zeichenketten werden nicht akzeptiert. (REQ-614) Einfaches INSERT, UPDATE und DELETE wird nicht auf einen Schreibpfad geroutet. (REQ-615) Schreiben Sie Daten über COPY FROM STDIN (beschreibbare Quellen) oder CREATE TABLE AS; zeilenbasierte Mutationen laufen stattdessen über die GraphQL-, Cypher- oder Trino-Schreibpfade.
COPY und DDL erfordern die Capability ddl. Sowohl COPY (in beide Richtungen) als auch DDL sind an die Capability ddl der Rolle gebunden; Rollen ohne diese erhalten SQLSTATE 42501. (REQ-616)
Keine echte Transaktionsunterstützung. BEGIN/COMMIT/ROLLBACK werden akzeptiert und stillschweigend ignoriert. Jede Anweisung wird unabhängig ausgeführt. (REQ-587) [tool-verified: server.py:146-158 — in_transaction() liefert stets False]
60-Sekunden-Timeout für DDL, 120-Sekunden-Timeout für Abfragen. Diese Werte sind in den Handler-Threads fest codiert. (REQ-590) Lang laufendes DDL gegen entfernte Quellen (Schemaänderungen an großen Tabellen) kann in ein Timeout laufen. [tool-verified: ddl_handler.py:136, server.py:186]
COPY FROM funktioniert nur mit beschreibbaren Quellen. Iceberg, Hive, reine Trino-Quellen und schreibgeschützte Quellentypen akzeptieren kein COPY FROM. Der Fehler ist SQLSTATE 42501. (REQ-586) [tool-verified: copy_handler.py:65]
Das COPY-Ausgabeformat ist text oder csv. Das binäre PG-COPY-Format (FORMAT binary) ist nicht implementiert. [inferred: In _rows_to_copy_text / _rows_to_copy_csv existieren nur die Zweige text und csv]
DDL auf dem Trino-Pfad ist auf CREATE beschränkt. ALTER, DROP und CREATE INDEX gegen Iceberg- oder Hive-Kataloge werden nicht unterstützt. Verwenden Sie eine registrierte SQL-Quelle als ddl_catalog, wenn Sie vollständiges DDL benötigen. (REQ-582) [tool-verified: ddl_handler.py:92-100]
Die Parametersubstitution erfolgt literal. Die Parameter $1, $2, ... werden vor der Ausführung als SQL-Literale eingesetzt, nicht als Bind-Parameter an die zugrunde liegende Engine gesendet. Das bedeutet, die zugrunde liegende Engine sieht niemals eine vorbereitete Anweisung. Für Trino hat dies keine praktische Auswirkung; bei Direct-Pool-Quellen umgeht es das Caching vorbereiteter Anweisungen. (REQ-581) [tool-verified: server.py:78-85]
pg_stat_activity, pg_stat_user_tables, pg_extension, pg_enum, pg_attrdef, pg_proc. Diese Tabellen existieren in der Katalogschicht, sind aber leere Stubs. Monitoring-Tools, die sie abfragen, erhalten null Zeilen statt Fehler. (REQ-532) [tool-verified: catalog.py:519-535, catalog.py:639-934] (pg_index ist befüllt — siehe Katalog-Interception.)