Polygres

PolyGres

← Back to the PolyWire overview — PolyWire is the intelligent gateway for Postgres. This page is the full technical detail behind that: every outcome, use case, benchmark, and honestly-scoped capability, organized by what it does — Connect, Protect, Control, Accelerate, Observe.

01 ghcr.io/polygres26/polywire

PolyWire: full capabilities

On this page

Outcomes, by pillar

Everything below rolls up into the same five things PolyWire does — Connect, Protect, Control, Accelerate, Observe — grouped here instead of as one flat list, so it's clear which outcome each capability is actually buying you.

Connect

Simple architecture

One gateway for all protocols and data sources.

Governed AI access

An MCP frontend gives AI agents real Postgres access through the same SQL firewall, ACL, and QoS controls as every other client — not a separate, unguarded path to your data.

Easy to test

Write tests against each protocol's own real client library — psql, mongosh, boto3, python-oracledb, opensearch-py, and more — no PolyWire-specific SDK to learn first.

Protect

Strong security

Centralized policies, auth, and SQL firewall — see "Security" below for the full, honestly-scoped detail.

Control

Scale elastically

Add shards, regions, tenants without re-architecting.

Planned & unplanned downtime management

A scheduled drain waits for a clean cutover; an unrelated backend failure triggers automatic failover in seconds — the two cases are handled deliberately differently, across every wire protocol.

Accelerate

Max performance

Smart routing, QoS control, PolyCache, and rollups.

Lower costs

Better resource utilization and reduced complexity.

Observe

Operational excellence

Deep observability, audit, and real-time insights.

Native error translation

Every real Postgres failure comes back as that protocol's own genuine error — ORA-03113, a MongoDB codeName, a DynamoDB exception name — so each client's own retry/reconnect logic keys off exactly what it expects.

Use cases

Admin console

PolyWire's admin console: a Metrics dashboard showing live protocol traffic across all nine protocols including gRPC and OpenSearch shown as their own distinct rows, reads/writes per second, top SQL by cost, per-tool MCP call counts and error rates, traffic by backend (routing/sharding target), and a cache-hit-vs-Postgres RTT breakdown covering every protocol including OpenSearch, with a sidebar grouped into four real labeled sections — Monitoring (Metrics, Topology), Security (SQL Firewall, ACL, OAuth), Traffic (Backends, Queues, Data Explorer, Router Rules — where shard/routing targets are configured, QoS), and Configuration (LLM)
PolyWire ships its own admin app — live traffic metrics across every protocol (gRPC and MCP shown separately, not merged into one label), per-tool MCP call counts and error rates, cache-hit-vs-Postgres RTT for every protocol including OpenSearch, and a sidebar that groups controls the same way this page does: Security (SQL firewall, ACL, OAuth) as its own labeled section, Router Rules for sharding/routing targets under Traffic, deployment topology, backends, queues, and LLM configuration for dialect translation — all in one place, not scattered across each protocol's own tooling.

PolyCache hit vs. Postgres round trip, per protocol

Up to 9.3× faster repeat reads — built-in distributed caching reduces Postgres round trips across every protocol PolyWire speaks.

See benchmark methodology and full numbers →

PolyCache is PolyWire's embedded, distributed query result cache — the same admin console tracks this per wire protocol: how long a cache hit takes against a real Postgres read or write. Measured on a local loopback deployment (client, PolyWire, and Postgres all on one machine), 30+ samples per cell — every protocol's connections were opened together and warmed with an equal number of throwaway calls each before any sample was taken, then measured in round-robin order (one call per protocol, repeated) rather than one protocol's whole run before the next. An earlier pass here that measured each protocol in its own sequential block produced skewed numbers — whichever protocol ran first absorbed the JVM's own warm-up cost — so it's corrected below. The point isn't the absolute millisecond figures, which shrink further over a real network, it's that the cache consistently wins on every protocol PolyWire speaks, without any application-side caching code, and that a real Postgres round trip costs about the same regardless of which wire protocol asked for it.

Protocol Cache hit Postgres read Postgres write Cache speedup
Postgres (pgwire) 0.08 ms 0.53 ms 0.83 ms 6.7×
SQL Server (mssqlwire) 0.08 ms 0.60 ms 0.93 ms 8.0×
MySQL (mywire) 0.08 ms 0.74 ms 0.73 ms 9.3×
Oracle (orawire) 0.08 ms 0.57 ms 0.60 ms 7.6×
MongoDB (mongowire) 0.10 ms 0.61 ms 1.48 ms 5.9×
DynamoDB (dynamowire) 0.07 ms 0.60 ms 0.95 ms 9.0×
OpenSearch (oswire) 1.24 ms 1.13 ms

Workload, exactly: for the four SQL protocols, SELECT * FROM t against a cached row (cache hit), the same query against an uncached row (Postgres read), and UPDATE t SET id = id (Postgres write) — issued through each protocol's real driver (psycopg2, python-tds/pymssql, python-oracledb, the MySQL CLI), not a synthetic benchmark harness. For MongoDB, findOne by _id (mongowire's cache only covers exact-_id lookups) against a cached vs. uncached document, and insertOne for the write. For DynamoDB, GetItem by primary key (same exact-key-only caching) against a cached vs. uncached item, and PutItem for the write. For OpenSearch, a term _search and an index (upsert) call via the real opensearch-py client — oswire has no result cache at all (see the rollups/caching architecture above; every _search/_doc call hits Postgres directly), so its cache-hit and speedup cells are genuinely blank, not omitted data.

Sharding: by real bound values, across every protocol that has one

Real cross-shard merge — weighted averages and centrally sorted/paginated results, not concatenation — across SQL and document/search protocols alike.

See sharding detail →

PolyWire routes by a real bound parameter value — not just SQL text — across Postgres, Oracle, SQL Server, and MySQL wire protocols, decoding each protocol's own native binary bind-parameter encoding (Postgres extended-query, Oracle, SQL Server sp_executesql, MySQL COM_STMT_EXECUTE) so a client's PreparedStatement value — tenant_id, customer_id, whatever the shard key is — reaches the router correctly, whichever of the four protocols the client happens to speak. A client that sends the same value as a plain SQL literal (psql, simple-query mode, an ORM that doesn't bind parameters) still routes correctly too, via literal-value matching — the feature degrades to "still correct," not "silently wrong." Hash, consistent-hash (150 virtual nodes per backend, minimizing what actually moves when the shard set changes), range, and list strategies are all real, configured via the same POLYWIRE_ROUTER_VALUE_SHARD_RULES knob regardless of which protocol or bind style the traffic arrives as.

A cross-shard SQL query — COUNT/SUM/AVG/ MIN/MAX, with or without GROUP BY, and ORDER BY/LIMIT/OFFSET — is genuinely merged across shards, not concatenated: AVG is a real weighted average (not an average-of-averages), and a global LIMIT caps the merged result, not each shard's own contribution. A query shape outside that set is refused with a clear error rather than silently mis-merged.

Sharding isn't SQL-only. DynamoDB (dynamowire) hashes by the real DynamoDB partition key. MongoDB (mongowire) hashes by _id for a {_id: ...}-shaped query; anything broader scatter-gathers. SQS (sqswire) hashes by queue name (one queue lives entirely on one backend — there's no per-message key to shard by independently). OpenSearch (oswire) hashes documents by doc_id, and a structured _search — hits, pagination, and terms/metric aggregations including a real weighted avg, even nested inside a terms bucket — merges across shards the same way SQL's does. k-NN (vector) and hybrid search on a sharded collection are refused with a clear error rather than quietly searching only one shard — a real, disclosed boundary, not a silent gap.

What sharding doesn't do: there's no elastic/online resharding anywhere — changing the shard set doesn't migrate data, queues, or documents already placed under the old set. Consistent hashing minimizes what would need to move; nothing moves it automatically today.

SQS: enqueue vs. dequeue

sqswire has no result cache — a queue's whole point is that every message is a real state change, nothing is safely repeatable from a cache — so instead of the cache-hit breakdown above, it reports the two halves of the queue lifecycle separately. Workload: SendMessage (enqueue) and ReceiveMessage (dequeue) via boto3's SQS client, 40 samples each, warmed and round-robined the same way as the table above. sqswire runs the AWS-style JSON-over-HTTP API real SQS also uses, not a raw binary wire protocol like the other six — the modest premium over the table above (roughly 1.5–2x a Postgres round trip via pgwire, not the much larger gap an earlier, less careful measurement pass here showed) is HTTP request/response framing and JSON parsing on every call, not the queue logic itself: SendMessage and ReceiveMessage are each a single SQL statement against Postgres, same as the tables above.

Protocol Enqueue (SendMessage) Dequeue (ReceiveMessage)
Amazon SQS (sqswire) 1.18 ms 1.25 ms

Rollups: pre-aggregated tables, kept fresh automatically

Define a rollup once — PolyWire keeps it fresh and rewrites matching queries to use it automatically, no application change.

See rollup configuration and detail →

A rollup is a real Postgres table PolyWire creates and keeps current for you — the materialized result of a GROUP BY/aggregate query you define once, not a view and not something your application maintains by hand. Define it in YAML (a POLYWIRE_ROLLUP_DEFINITIONS_FILE, or the same field via the admin API):

rollups: - name: daily_order_totals backend: primary source_table: orders group_by: - customer_id - order_date aggregations: - "SUM(amount) AS total_amount" - "COUNT(*) AS order_count" refresh_interval_minutes: 15 max_staleness_minutes: 30

Every 15 minutes, PolyWire runs exactly the SQL you'd write by hand to keep this current — DROP TABLE IF EXISTS polywire_rollup_daily_order_totals; then CREATE TABLE polywire_rollup_daily_order_totals AS SELECT customer_id, order_date, SUM(amount) AS total_amount, COUNT(*) AS order_count FROM orders GROUP BY customer_id, order_date; — against the real orders table, on the backend named primary. max_staleness_minutes is the cutoff past which PolyWire stops trusting that table for acceleration until the next refresh succeeds.

Acceleration is automatic and always on for a fresh, matching rollup — there's no separate switch to flip. PolyWire embeds a genuine SQL parser, validator, and relational query planner inside its own pipeline. When a client sends an aggregate query against orders, PolyWire parses it, checks whether a fresh rollup's source table is mentioned, and rewrites the query to read from polywire_rollup_daily_order_totals instead whenever its materialized-view matcher can prove that substitution is valid for that specific query shape — defining the rollup above is turning rewriting on for it, nothing else to configure. Your application still just queries orders — it never has to know the rollup exists. If the rollup is stale, doesn't apply, or the rewrite can't be proven safe for that query, PolyWire falls straight through to the real table unchanged — a rollup can only ever make a matching query faster, never make an unmatched one wrong.

Which protocols this applies to: rollup acceleration lives in the same shared pipeline as the SQL firewall and dialect translation, so it covers every protocol that sends a SQL statement through that pipeline — pgwire, mywire, mssqlwire, orawire, gRPC, and MCP's execute_sql tool. mongowire, dynamowire, and sqswire don't build SQL text at all — a Mongo find or a DynamoDB GetItem goes straight from that protocol's own store layer to Postgres, so there's no SQL statement for a rollup to match against and no rewriting happens for those three today.

Observability: CloudWatch, Datadog, New Relic, Grafana, and more

Prometheus scrape, direct OTLP push, or via an OTel Collector — plugs into the monitoring you already run.

See export surfaces and per-platform detail →

PolyWire exposes two real, independent export surfaces — a Prometheus-format /metrics endpoint and a periodic OTLP metrics push, over either gRPC (:4317, the default) or HTTP (:4318, set POLYWIRE_OTEL_PROTOCOL=http) — and how each platform gets that data differs by platform. Some scrape /metrics directly, some accept an OTLP push with no extra hop, and some (CloudWatch, Azure Monitor, GCP Cloud Monitoring, AppDynamics) need an OpenTelemetry Collector in between to translate into their native API. OTLP/HTTP exists for the networks gRPC doesn't reach cleanly — corporate proxies and L7 load balancers that only forward plain HTTP/HTTPS. This is metrics only today — no traces or logs — and PolyWire doesn't set a service.name resource attribute, so a receiving platform identifies its data by the polywire_* metric-name prefix, not a service tag.

PolyWire observability integration diagram: PolyWire exposes /metrics :19090, OTLP/gRPC :4317, and OTLP/HTTP :4318. A green Prometheus-scrape path runs through a scrape agent to Self-hosted Grafana, Grafana Cloud, and Datadog's Prometheus check. A blue direct-OTLP-push path runs straight to New Relic, Datadog's native OTLP ingest, and Grafana Cloud's OTLP gateway. An orange dashed via-Collector path runs through an OpenTelemetry Collector to AWS CloudWatch, Azure Monitor, GCP Cloud Monitoring, and AppDynamics
How PolyWire's metrics reach each platform: a Prometheus scrape, a direct OTLP push, or a push routed through an OpenTelemetry Collector, depending on what the destination platform natively accepts.
Env varDefaultWhat it does
POLYWIRE_OTEL_PROTOCOLgrpcgrpc or http
POLYWIRE_OTEL_ENDPOINThttp://localhost:4317Collector/backend endpoint (defaults to :4318 when protocol=http)
POLYWIRE_OTEL_EXPORT_INTERVAL_MS5000Push interval
POLYWIRE_OTEL_HEADERSComma-separated key=value request headers (e.g. an API key)
POLYWIRE_METRICS_PORT19090Prometheus /metrics scrape port
POLYWIRE_ADMIN_TOKENBearer auth on /metrics, if set
Scrape

Self-hosted Grafana

Point a Prometheus server (or Grafana Agent/Alloy) at /metrics and add it as a Grafana data source — no collector, no OTLP.

ScrapeDirect OTLP

Grafana Cloud

Either scrape /metrics and remote_write it in, or push OTLP straight to Grafana Cloud's own OTLP gateway.

ScrapeDirect OTLP

Datadog

The Datadog Agent can scrape /metrics as a Prometheus check, or take a native OTLP push on recent Agent versions — either way, no collector required.

Direct OTLP

New Relic

Push OTLP straight to otlp.nr-data.net:4317 with your API key in POLYWIRE_OTEL_HEADERS. No collector hop.

Via Collector

AWS CloudWatch

No native OTLP receiver — run an OpenTelemetry Collector (e.g. ADOT) with the awsemf exporter in between.

Via Collector

Azure Monitor / App Insights

Same shape — a Collector with the azuremonitor exporter and your connection string.

Via Collector

GCP Cloud Monitoring

A Collector with the googlecloud (or googlemanagedprometheus) exporter forwards into Cloud Monitoring.

Via Collector

AppDynamics

Historically agent/controller-based rather than an open metrics receiver — route through a Collector into Cisco Cloud Observability.

Multi-AZ deployment

PolyWire multi-AZ cloud deployment: client applications behind a network load balancer, stateless instances per zone holding primary or backup cache copies, a config-primary Postgres with standby failover
Every piece of this is real and tested today, including the cross-zone cache backup replication — a cache entry's backup copy is placed on a node in a different availability zone than its primary, proven by a live test with real cache nodes. Cluster discovery works across a static seed list or AWS/GCP/Azure storage; connections between cache nodes can be TLS-encrypted.

Security

Every layer below is real, running code — not roadmap. Each item states its actual scope plainly, including the parts that are opt-in or narrower than the feature name might suggest; a security page that only lists what's strong isn't one you should trust.

Row-level security, for real — your own Postgres RLS policies do the filtering, not PolyWire rewriting SQL.

See RLS detail and requirements →

Row-level security, for real: under POLYWIRE_AUTH_MODE=postgres_roles (Postgres, SQL Server) or POLYWIRE_AUTH_CREDENTIALS (Oracle), every login sets polywire.user_id as a session GUC on the backend connection before each statement — filtering is enforced by your own Postgres RLS policies (USING (owner_user = current_setting('polywire.user_id'))-style), not by PolyWire rewriting SQL. This holds even fronted by the Oracle wire protocol: orawire's SQL translation always executes against Postgres, so the same Postgres RLS mechanism applies — no separate Oracle VPD/SYS_CONTEXT setup needed, since there's no real Oracle database in this path for VPD to run against. One requirement RLS makes non-negotiable: PolyWire's own backend role must not be a superuser or the table owner — either bypasses every RLS policy unconditionally, no matter what the session GUC says. Grant it SELECT on pg_authid and on the tables it serves instead. Every login — success or failure, real identity or shared credential — is recorded to a tamper-evident audit log, readable live via the bearer-token-authenticated GET /api/audit admin route. Column-masking and attribute-based row filtering beyond native RLS (the AccessControlStage engine) are real, reviewed code, not yet wired to a config surface — out of scope until an identity-to-attribute mapping exists. MySQL and the DynamoDB/MongoDB/SQS/OpenSearch wire protocols don't yet propagate identity into RLS at all — every session there still shares one backend credential.

Planned and unplanned outages

A Postgres backend going down for a patch shouldn't take your application down with it — whether you chose the timing or not. PolyWire tells the two cases apart deliberately: a planned switchover can afford to wait for a clean cutover; an unplanned failure can't wait for anything.

Every wire protocol participates — Postgres, SQL Server, MySQL, Oracle, MongoDB, DynamoDB, SQS, and OpenSearch alike. Drain/undrain, fallback redirection, and auto-failover all resolve backends through the same registry lookup every protocol's store now shares. A backend that starts serving traffic for the first time after a switchover — a fallback that's a genuinely separate Postgres, not necessarily a replica — gets its schema/catalog tables created there automatically the first time routing actually lands on it, not never.

Error handling: real native errors, not a generic wrapper

Underneath every protocol PolyWire speaks, the real failure is a Postgres SQLSTATE — a unique-constraint violation, a missing table, a dead connection. A driver's own retry and reconnect logic (Oracle drivers checking for ORA-03113 specifically to decide whether to transparently reconnect, MongoDB's retryable-reads keying off a real codeName, the AWS SDK unmarshalling a real exception class) only works if PolyWire sends the exact native error that driver expects — not a generic internal error that leaves that logic with nothing to act on. Every wire protocol translates the real Postgres SQLSTATE into that protocol's own genuine error shape, verified against each vendor's own documentation or a real client library actually parsing the response, not guessed.

Where a vendor genuinely has no dedicated code for a condition (SQL Server has no numbered error for a mid-session transport failure; SQS has no code distinct from a generic backend failure for a duplicate/throttling condition), that gap is documented plainly rather than papered over with an invented code. Full per-protocol mapping tables, the canonical- condition comparison, and the connection-loss detail live in ERRORS.md.