Skip to content

Postgres Wire Protocol

QueryMode includes a PostgreSQL wire protocol server. Any tool that connects to Postgres — psql, DBeaver, Metabase, Grafana, TablePlus, DataGrip — can query your data without code changes.

Terminal window
# Start the pg-wire server (reads files from current directory)
npx tsx src/pg-wire/server.ts
# Connect with psql
psql -h localhost -p 5433 -U querymode

Then query as normal SQL:

SELECT * FROM './data/events.parquet' WHERE region = 'us' LIMIT 10;
SELECT region, COUNT(*), AVG(amount) FROM './data/orders.lance' GROUP BY region;
Environment variableDefaultDescription
PG_PORT5433Listen port
PG_HOST127.0.0.1Bind address
Terminal window
PG_PORT=5432 PG_HOST=0.0.0.0 npx tsx src/pg-wire/server.ts

The pg-wire server uses QueryMode’s SQL parser, so all SQL syntax is supported:

  • SELECT, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET
  • JOIN (inner, left, right, full, cross)
  • Window functions (ROW_NUMBER, RANK, LAG, LEAD, rolling aggregates)
  • CTEs (WITH ... AS)
  • Set operations (UNION, INTERSECT, EXCEPT)
  • Vector search (WHERE embedding NEAR [...] TOPK 10)
  • All 14 filter operators, CASE, CAST, arithmetic, || concatenation
  • -- line comments and /* */ block comments

These are accepted and acknowledged for BI tool compatibility but have no effect:

  • SET client_encoding TO 'UTF8'
  • RESET ALL
  • DISCARD ALL
  • SHOW <parameter> (returns "on" for all parameters)
  • BEGIN, START TRANSACTION, COMMIT, END, ROLLBACK, ABORT (auto-commit mode)
  • DEALLOCATE, CLOSE (no-op since prepared statements are not stored)

The PgConnectionHandler class can be embedded in any Node.js TCP server or WebSocket bridge:

import { QueryMode } from "querymode/local"
import { PgConnectionHandler } from "querymode"
import * as net from "node:net"
const qm = QueryMode.local()
net.createServer((socket) => {
const handler = new PgConnectionHandler({
executor: qm.getExecutor(),
send: (data) => socket.write(data),
})
socket.on("data", async (chunk) => {
await handler.onData(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength))
})
}).listen(5433)

The handler manages the full connection lifecycle:

  1. Startup — SSL negotiation (rejected), authentication (always ok), server parameters
  2. Query — SQL parsing, compilation, execution via the QueryMode pipeline
  3. ResponseRowDescription + DataRow messages with proper type OIDs
  4. Error — SQL errors return ErrorResponse with proper SQLSTATE codes and the connection stays open

Errors include standard PostgreSQL SQLSTATE codes so BI tools can classify them:

QueryMode errorSQLSTATEMeaning
TABLE_NOT_FOUND42P01Undefined table
COLUMN_NOT_FOUND42703Undefined column
INVALID_FILTER, INVALID_AGGREGATE42601Syntax error
SCHEMA_MISMATCH42804Datatype mismatch
INVALID_FORMAT08P01Protocol violation
QUERY_TIMEOUT, NETWORK_TIMEOUT57014Query canceled
MEMORY_EXCEEDED53200Out of memory
QUERY_FAILEDXX000Internal error
Other42000Generic syntax error (default)

QueryMode types map to PostgreSQL type OIDs:

QueryMode typePostgres typeOID
int8, int16, uint8, uint16INT221
int32, uint32INT423
int64, uint64INT820
float16, float32FLOAT4700
float64FLOAT8701
utf8, stringTEXT25
boolBOOL16
binaryBYTEA17
  • Simple Query protocol — extended query protocol messages (Parse, Bind, Describe, Execute) are acknowledged but not executed. Sync is handled correctly. Most BI tools fall back to Simple Query automatically.
  • No authentication — all connections are accepted. Run behind a firewall or SSH tunnel for production use.
  • No SSL/TLS — SSL requests are rejected; clients retry in plaintext. Use an SSH tunnel or reverse proxy for encrypted connections.
  • Local mode only — the built-in server uses LocalExecutor. For edge mode, build a custom server that passes a RemoteExecutor to PgConnectionHandler.