Skip to content

feat(postgres-wire-features): add Postgres v3 wire-protocol sample#229

Merged
slayerjain merged 7 commits intomainfrom
postgres-wire-features
Apr 24, 2026
Merged

feat(postgres-wire-features): add Postgres v3 wire-protocol sample#229
slayerjain merged 7 commits intomainfrom
postgres-wire-features

Conversation

@Aditya-eddy
Copy link
Copy Markdown
Member

@Aditya-eddy Aditya-eddy commented Apr 24, 2026

Summary

Adds postgres-wire-features/, a stdlib-only Go HTTP service that speaks the Postgres v3 frontend/backend protocol directly (no driver). It packages ten Postgres feature areas behind GET /run/all and GET /case/<name> so a single Keploy record/replay pass covers a wide surface of wire-protocol message types in one sample.

Scenario Statements exercised
setup DROP TABLE, CREATE TABLE, INSERT, CREATE TABLE IF NOT EXISTS
dml INSERT … ON CONFLICT DO UPDATE, UPDATE, DELETE, MERGE
catalog-cte WITH … SELECT FROM pg_catalog.pg_class
catalog-sub SELECT … FROM (SELECT …) AS s
catalog-setop SELECT … UNION SELECT …
copy TRUNCATE, COPY … FROM STDIN, COPY … TO STDOUT
prepare PREPARE, EXECUTE, DEALLOCATE
cursor BEGIN, DECLARE CURSOR, FETCH×2, CLOSE, COMMIT
admin ANALYZE, REINDEX, LOCK TABLE, DO $$ … $$, CREATE PROCEDURE, CALL
validation-ping SELECT 'ok', SELECT true, SELECT NULL, SELECT 1

Each scenario's JSON response carries the per-statement command tag, rows, and (for COPY TO STDOUT) the copyData payload, so replay diffs localise cleanly to the Postgres statement that differed.

Why stdlib / no driver

Driver libraries hide a number of wire-level details (extended-query portals, CopyData / CopyDone framing, raw command tags). Talking the v3 protocol directly over net.Conn means the recorded Postgres traffic is what Keploy's proxy actually saw, which is the point of a wire-protocol sample.

Layout

Mirrors the http-postgres/ convention:

  • main.go — server + scenarios
  • Dockerfile — two-stage (golang:1.24-alpine → alpine:3.19)
  • docker-compose.ymlapi + db (postgres:16-alpine) with healthcheck
  • test.sh — traffic script (/health, /run/all, each /case/<name>)
  • README.md — record & replay instructions

Related

  • Pipeline: keploy/integrations#134
  • Tracking issue (v3 gaps this sample exposes): keploy/enterprise#1921

Test plan

  • docker compose up --build brings the stack up and curl http://localhost:8080/health returns ok
  • ./test.sh exercises every /case/<name> and /run/all
  • keploy record -c "docker compose up" --container-name api --delay 10 captures HTTP tests + Postgres mocks under keploy/
  • keploy test -c "docker compose up" --container-name api --delay 20 replays; diffs are reported per-scenario in the JSON body

🤖 Generated with Claude Code

A stdlib-only Go HTTP service that drives a PostgreSQL connection directly
over the v3 frontend/backend protocol (no driver), exposing ten scenarios
under GET /run/all:

- setup (DDL)
- dml (INSERT ON CONFLICT, UPDATE, DELETE, MERGE)
- catalog-cte / catalog-sub / catalog-setop (pg_catalog queries via CTE,
  subselect, UNION)
- copy (COPY FROM STDIN + COPY TO STDOUT)
- prepare (PREPARE, EXECUTE, DEALLOCATE)
- cursor (BEGIN, DECLARE CURSOR, FETCH, CLOSE, COMMIT)
- admin (ANALYZE, REINDEX, LOCK TABLE, DO, CREATE PROCEDURE, CALL)
- validation-ping (SELECT 'ok', SELECT true, SELECT NULL, SELECT 1)

Standard sample layout: Dockerfile, docker-compose.yml with api+db,
test.sh traffic script that hits /run/all and each /case/<name>, and
a README with record/test instructions.

The wire-protocol traffic is intentionally low-level so Keploy's proxy
sees every message class (CopyData, CopyDone, Parse/Bind/Execute, cursor
portals, etc.) as the server sends them, rather than what a driver would
hide behind its API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Aditya-eddy <aditya282003@gmail.com>
@Aditya-eddy Aditya-eddy force-pushed the postgres-wire-features branch from a2de363 to 7d89cc7 Compare April 24, 2026 09:24
Aditya-eddy and others added 4 commits April 24, 2026 15:16
…only

Dropping the per-scenario /case/<name> loop from test.sh. /run/all already
exercises every scenario inside one HTTP request, which is all a record
pass needs; firing /case/* endpoints in quick succession afterwards
created many back-to-back raw-socket Postgres connections that could
push the recording proxy into an EOF regime in docker mode, producing
noisy captures whose replay diffs drowned out the real signal.

Signed-off-by: Aditya-eddy <aditya282003@gmail.com>
…un/all

Refactor: runXxx scenario functions now take *pgClient instead of opening
their own. /run/all dials once and walks all ten scenarios on that single
connection; /case/<name> keeps its own withClient wrapper so individual
scenario endpoints still run standalone.

Opening a fresh connection per scenario pushed Keploy's docker-mode proxy
into a back-to-back connect/close regime where the agent dropped
handshakes (visible in CI logs as 'failed to read client auth response:
EOF' on every other connection during record, and 'replayer: session
RecordedIndex missing PostgresV3Session mock' during replay). Sharing one
connection drops the connect count from ~10 per /run/all to 1, which
keeps the v3 wire surface exercised — CopyData/CopyDone frames are still
sent on the wire; validation-ping SELECTs still collide on sqlAstHash —
without tripping that unrelated instability.

Signed-off-by: Aditya-eddy <aditya282003@gmail.com>
…ndshake

Postgres 16 defaults to scram-sha-256 (auth code 10) when a password is
set. This sample's pure-stdlib client only implements codes 0/trust,
3/cleartext, and 5/md5 — SASL is out of scope for a wire-protocol repro.
Pinning POSTGRES_HOST_AUTH_METHOD=trust keeps auth on code 0, which is
what the client actually speaks.

Observed in CI: dial was failing with 'unsupported postgres
authentication code 10' before any scenario query ran, so the recorded
test body was just the auth error — masking the real Postgres wire
surface the sample is meant to exercise.

Signed-off-by: Aditya-eddy <aditya282003@gmail.com>
…ios and assertions

Extend the sample with two new scenarios that exercise specific v3
recorder edges not covered by the existing suite:

- /case/multistatement sends BEGIN; SELECT 1 AS a; SELECT 2 AS b; COMMIT
  as a SINGLE simple-Query packet. A recorder that splits the batch with
  pg_query emits four invocations; a recorder that tracks per-packet
  emits one.
- /case/empty-query sends a Query packet with empty SQL body. The server
  answers with EmptyQueryResponse ('I') and ReadyForQuery only. A
  correct recorder suppresses this as a no-op; a naive recorder emits a
  ghost invocation with empty sqlNormalized and class=UNKNOWN.

Both scenarios are added to /run/all so the single-connection path
exercises them, and are also exposed as standalone /case endpoints so
their recorded testcases are isolated by connection boundary.

simpleQuery grows an 'I' (EmptyQueryResponse) case so the empty-query
path returns cleanly rather than erroring on the unhandled tag.

assertions.sh validates recorded mocks.yaml after a Keploy record run:

1. no invocation with class: UNKNOWN
2. no invocation with empty sqlNormalized (ghost event)
3. multistatement Q packet produced >=1 each of BEGIN, COMMIT,
   SELECT \$1 AS a, SELECT \$1 AS b invocations
4. at least some readable SQL is present (mocks aren't entirely base64)

Use ./assertions.sh keploy after keploy record to fail the pipeline if
the recorder regressed on any of those invariants.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new postgres-wire-features/ Go sample: a stdlib-only HTTP service that speaks the PostgreSQL v3 wire protocol directly to exercise a broad set of message types/scenarios for Keploy record/replay.

Changes:

  • Introduces a minimal HTTP server (/health, /run/all, /case/<name>) that runs multiple Postgres scenarios over a raw TCP v3 protocol client.
  • Adds Docker Compose + Dockerfile for running the API alongside Postgres 16, plus traffic-generation and post-record invariants scripts.
  • Documents usage (record/replay, scenarios, environment variables) and adds local ignores for Keploy artifacts.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
postgres-wire-features/main.go Implements HTTP endpoints, scenario suite, and a raw Postgres v3 frontend/backend client.
postgres-wire-features/docker-compose.yml Defines api + db services with healthcheck and environment wiring.
postgres-wire-features/Dockerfile Builds and packages the sample as a small container image.
postgres-wire-features/go.mod Declares the module and Go version for the new sample.
postgres-wire-features/README.md Documents scenarios, endpoints, record/replay flow, and configuration.
postgres-wire-features/test.sh Generates HTTP traffic to exercise /run/all and key edge cases.
postgres-wire-features/assertions.sh Validates recorded mocks against expected v3 recorder invariants.
postgres-wire-features/.gitignore Ignores Keploy outputs and local build artifacts for this sample.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

# record/replay cycle is actually exercising.
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- "5433:5432"
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POSTGRES_HOST_AUTH_METHOD: trust combined with publishing the DB port (5433:5432) makes the database accept unauthenticated connections from the host network. Consider either (a) not publishing the DB port at all, (b) binding it to localhost only (e.g. 127.0.0.1:5433:5432), or (c) switching to md5 auth (supported by this client) so the sample isn’t running an open Postgres on the developer machine.

Suggested change
- "5433:5432"
- "127.0.0.1:5433:5432"

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +7
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With POSTGRES_HOST_AUTH_METHOD=trust, the POSTGRES_PASSWORD/PGPASSWORD values are effectively unused (server won’t request a password). This can confuse readers and can mask an accidental reliance on trust auth; consider aligning these by using md5 auth (and keeping passwords) or removing the password env vars if trust is intentional.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +18
echo -e "\n--- /health (sanity) ---"
curl -s -w " HTTP %{http_code}\n" "$BASE_URL/health"

# /run/all exercises every scenario (setup, dml, catalog-{cte,sub,setop},
# copy, prepare, cursor, admin, validation-ping, multistatement,
# empty-query) inside a single HTTP request. That keeps the recorded HTTP
# testcase count small while still driving the full Postgres wire surface
# Keploy needs to see.
echo -e "\n--- /run/all (one call exercises every scenario) ---"
curl -s -w "\n HTTP %{http_code}\n" "$BASE_URL/run/all" -o /tmp/run-all.json
echo " response bytes: $(wc -c </tmp/run-all.json)"
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script prints HTTP status codes but doesn’t fail the run when an endpoint returns a non-2xx/3xx response (curl exits 0 on HTTP 500 unless --fail/--fail-with-body is used). Since these endpoints are expected to succeed, consider making the script exit non-zero on unexpected status codes so CI/local runs don’t silently pass while generating bad recordings.

Copilot uses AI. Check for mistakes.
Comment on lines +313 to +324
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), 10*time.Second)
if err != nil {
return nil, err
}

c := &pgClient{conn: conn, user: user}
if err := c.startup(user, db, password); err != nil {
conn.Close()
return nil, err
}
return c, nil
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After dialing, the connection has no read/write deadlines; readMessage uses io.ReadFull, so a stalled/half-open Postgres connection can block the HTTP handler indefinitely. Consider setting per-operation deadlines (e.g., SetDeadline/SetReadDeadline/SetWriteDeadline) around startup + each query, or at least a global idle deadline, to avoid hung requests and leaked goroutines in failure cases.

Copilot uses AI. Check for mistakes.
Comment thread postgres-wire-features/main.go Outdated
return err
}
default:
return fmt.Errorf("unsupported postgres authentication code %d", code)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error unsupported postgres authentication code %d doesn’t give the user an actionable remediation (e.g., Postgres 16 default SCRAM is code 10). Consider including a hint like “configure Postgres auth to md5/trust (POSTGRES_HOST_AUTH_METHOD)” so users can quickly fix local runs when auth negotiation fails.

Suggested change
return fmt.Errorf("unsupported postgres authentication code %d", code)
return fmt.Errorf("unsupported postgres authentication code %d; this server may require SCRAM authentication (code 10). For local runs, configure Postgres auth to md5 or trust, for example with POSTGRES_HOST_AUTH_METHOD, or use a server/user configuration compatible with this client", code)

Copilot uses AI. Check for mistakes.
…TP errors

Address Copilot review on #229:

- docker-compose.yml: bind the container's 5433 port to 127.0.0.1 only
  instead of all interfaces. With POSTGRES_HOST_AUTH_METHOD=trust the DB
  accepts unauthenticated connections, so publishing on 0.0.0.0 left an
  open Postgres on whatever network the developer machine was attached
  to. Loopback-only keeps the sample usable for record/replay without
  that exposure.

- docker-compose.yml: remove POSTGRES_PASSWORD / PGPASSWORD. Under trust
  auth the server never requests a password; keeping those env vars
  suggested the auth path in use required one and could mask an
  accidental reliance on trust mode. Trust is intentional here (see the
  adjacent comment block) so the password vars are now dropped.

- test.sh: pass --fail-with-body to every curl invocation so an HTTP
  5xx from /health, /run/all, or the /case endpoints exits the script
  non-zero. Without this curl exits 0 on 500s and the traffic script
  silently produces a bad recording.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
…ed auth codes

Address Copilot review on #229:

- Add a 30s per-operation deadline around startup() and simpleQuery().
  Before this, the raw net.Conn had no deadlines, so a stalled or
  half-open Postgres connection would wedge io.ReadFull inside
  readMessage and hang the HTTP handler (and leak goroutines) until the
  OS eventually gave up. setOpDeadline / clearOpDeadline arm the
  deadline on entry and clear it on exit, so the /run/all path (which
  shares one pgClient across scenarios) never inherits a stale
  deadline between operations.

- Replace the opaque "unsupported postgres authentication code %d"
  error with a message that spells out the codes this client actually
  speaks (0/trust, 3/cleartext, 5/md5) and points at
  POSTGRES_HOST_AUTH_METHOD as the quick local remediation when the
  server negotiates scram-sha-256 (code 10, the Postgres 16 default).
  The prior message sent users digging through protocol docs to
  discover what the numeric code meant.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +99 to +103
Start Postgres yourself (e.g. `docker run -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:16-alpine`) and then:

```bash
PGHOST=127.0.0.1 PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres PGDATABASE=postgres go run .
```
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Run Natively" example uses the default Postgres image password auth, which commonly negotiates SCRAM (auth code 10). This client explicitly errors on SCRAM and only supports auth codes 0/3/5, so the documented command is likely to fail. Update the README to show a compatible server config (e.g., trust or md5 via POSTGRES_HOST_AUTH_METHOD / pg_hba.conf) or clarify the requirement.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
module postgres-wire-features

go 1.24
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new Go sample directory isn't included in the golangci-lint workflow matrix (.github/workflows/golangci-lint.yml), so it won't be linted in CI. Add postgres-wire-features to the matrix to keep lint coverage consistent with other Go samples.

Copilot uses AI. Check for mistakes.
body.WriteByte(0)

msg := withLength(body.Bytes())
if _, err := c.conn.Write(msg); err != nil {
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

net.Conn.Write may write fewer bytes than requested; both the startup packet write and writeMessage ignore the returned byte count, which can result in truncated protocol messages and hard-to-debug failures. Consider using io.WriteFull (or a small write loop) to ensure the entire message is sent, and handle any short write as an error.

Suggested change
if _, err := c.conn.Write(msg); err != nil {
if _, err := io.WriteFull(c.conn, msg); err != nil {

Copilot uses AI. Check for mistakes.
Comment on lines +492 to +497
func (c *pgClient) writeMessage(tag byte, payload []byte) error {
msg := make([]byte, 1, 1+4+len(payload))
msg[0] = tag
msg = append(msg, withLength(payload)...)
_, err := c.conn.Write(msg)
return err
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writeMessage doesn't verify that the full buffer was written to the TCP connection. If a short write occurs, the Postgres backend will see a truncated frame and the protocol stream becomes unrecoverable. Use a full-write helper/loop and treat short writes as an error.

Suggested change
func (c *pgClient) writeMessage(tag byte, payload []byte) error {
msg := make([]byte, 1, 1+4+len(payload))
msg[0] = tag
msg = append(msg, withLength(payload)...)
_, err := c.conn.Write(msg)
return err
func (c *pgClient) writeFull(buf []byte) error {
for len(buf) > 0 {
n, err := c.conn.Write(buf)
if err != nil {
return err
}
if n <= 0 {
return io.ErrShortWrite
}
buf = buf[n:]
}
return nil
}
func (c *pgClient) writeMessage(tag byte, payload []byte) error {
msg := make([]byte, 1, 1+4+len(payload))
msg[0] = tag
msg = append(msg, withLength(payload)...)
return c.writeFull(msg)

Copilot uses AI. Check for mistakes.
@slayerjain slayerjain merged commit 9a03b3e into main Apr 24, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants