2 Commits

Author SHA1 Message Date
8472928102 feat(pgserver): Phase 4 — Extended Protocol (Parse/Bind/Execute)
pgx and most drivers default to PostgreSQL's Extended Protocol
(named prepared statements). Phase 2 only handled Simple Query,
so every pgx caller had to force `QueryExecModeSimpleProtocol` —
unworkable for a production deployment. This commit lands the
full Parse → Bind → Describe → Execute → Sync state machine,
enough that pgx (and any other libpq-protocol-v3 client) works
without any client-side knobs.

Implementation lives in `hbrtl/pgserver/extended.go`:

* Per-session caches `stmts map[string]*preparedStmt` and
  `portals map[string]*portal`, lazily allocated on first use.
  Stored as fields on `session` so they don't leak across
  connections.

* Parameters are inlined at Bind time via `substituteParams` —
  the resolved SQL is a normal Simple-Query-shaped string the
  engine sees through the existing `five_SQL(cSQL, …, oSession)`
  pipeline. Avoids teaching FiveSql2 a second param-shape; the
  trade-off is that binary timestamps/numerics round-trip through
  text (Phase 4.1 will plumb `?`-params through aParams for the
  binary fast path).

* `paramToLiteral` decodes the binary-format encodings pgx uses
  by default for INT4/INT8/BOOL (big-endian fixed-width). Other
  binary OIDs fall back to a hex-escaped quoted literal which
  errors loudly rather than silently misparsing.

* `countPgPlaceholders` scans the SQL outside string literals for
  the highest `$N` so the server can answer Describe-statement
  with a correctly-sized ParameterDescription even when the
  client didn't pre-declare param OIDs. Without this, pgx errored
  with "expected 0 arguments, got 2" on the very first prepared
  query.

* RowDescription emission: Describe-statement still returns NoData
  (we can't infer row shape without execution). When Execute fires
  on a portal the client never Described, we emit RowDescription
  inline from the cached result before DataRow streams. pgx and
  psql both tolerate this ordering.

* Execute → CommandComplete tag derives from the SQL verb via the
  existing `commandTagFor` helper. Row counts in the tag remain
  "VERB 0" for v1.0; threading real counters through the engine
  is Phase 5.

Wire dispatch in `session.go:queryLoop` now handles Parse, Bind,
Describe, Execute, Close, Sync, Flush — the full v3 message set.

Verification
------------

End-to-end pgx (default mode, no SimpleProtocol flag) successfully
runs:
  SELECT $1 AS n, $2 AS s with 42 + "hi" → [42 hi]
  Same statement re-executed with different bound values → reuses
    the cached prepared statement
  SELECT $1 AS b, $2 AS s with true + "binary-bool" → [t binary-bool]

`tests/pgserver/run.sh` expanded from 1 → 3 integration assertions:

  PASS  Simple Query: SELECT 1, 'hello'
  PASS  Multi-statement Simple Query
  PASS  Transaction control: BEGIN/COMMIT round-trip

(Extended Protocol can't be driven from psql's -c CLI directly
because psql's PREPARE/EXECUTE is a separate SQL-level feature
that FiveSql2 doesn't parse; the pgx-driven path verifies it
manually, and a self-contained Go integration that drives pgx
from inside a process bootstrap is Phase 7 work.)

All six release gates green:
  go test ./...                       ✓
  FiveSql2 SQL:1999 43/43             ✓
  Harbour compat 56/56                ✓
  std.ch 17/17                        ✓
  FRB 7/7                             ✓
  pgserver integration 3/3            ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:55:41 +09:00
d98f5e1767 feat(pgserver): PostgreSQL-wire MVP — psql can SELECT from FiveSql2
First end-to-end working version of the PostgreSQL-wire-compatible
TCP server frontend. A standard `psql` client now connects, runs
`SELECT * FROM employees`, and gets back a properly typed result
set rendered by psql with the right column alignment:

    ID |         NAME         |  SALARY
    ----+----------------------+----------
      1 | Alice                | 50000.00
      2 | Bob                  | 42000.50
      3 | Cho                  | 77500.00

This is the Phase 2 deliverable from the approved plan at
/Users/charleskwon/.claude/plans/compiled-launching-shore.md.
Builds on the session-state refactor in 93cf5c8 — each connection
gets its own TSqlSession on the PRG side via the new PG_NEW_SESSION
HB_FUNC, so concurrent psql clients won't share transaction logs
or plan caches.

Scope
-----

v1.0 MVP: Simple Query only, trust auth, no TLS yet. SELECT works
against the full FiveSql2 surface (CTEs, window functions, JOINs,
aggregates). DML + per-session transactions are Phase 3, extended
protocol is Phase 4, auth + TLS are Phases 5/6.

Architecture
------------

  psql/pgx/JDBC ──TCP:5432──▶ pgserver.Listener
                                  │ accept()
                                  ▼ go handleConn(net.Conn)
                             ┌─────────────────────────────┐
                             │ Session goroutine            │
                             │  1. SSLRequest peek          │
                             │  2. StartupMessage           │
                             │  3. AuthenticationOk (trust) │
                             │  4. ParameterStatus×7        │
                             │  5. BackendKeyData           │
                             │  6. ReadyForQuery('I')       │
                             │  7. loop: Receive() →        │
                             │     dispatchSimpleQuery →    │
                             │     hbrt.Thread.Function(    │
                             │       FIVE_SQL,sql,...,sess) │
                             │     emit RowDescription      │
                             │     emit DataRow×N           │
                             │     emit CommandComplete     │
                             │     emit ReadyForQuery       │
                             └─────────────────────────────┘

One goroutine per connection, each owning its own *hbrt.Thread and
TSqlSession instance. Uses the existing audit-fixed NewThread()
(cde8673) so statics + WA factory propagate.

New files (hbrtl/pgserver/)
---------------------------

* server.go — Config, Server, Serve loop with MaxConnections gate
  via semaphore, Close drains in-flight sessions.
* session.go — full lifecycle: SSLRequest peek + prefixedConn
  byte-injection trick for StartupMessage, ParameterStatus
  broadcast (server_version "14.0 (FiveSql2)" so pgx negotiates),
  BackendKeyData (random pid+secret per session, no CancelRequest
  yet), query loop dispatching only Simple Query in v1.0 with a
  loud "0A000 not supported" for Extended messages.
* dispatch.go — runSQL invokes FIVE_SQL via PushSymbol+Function,
  unpacks the engine's `{aFieldNames, aRows}` envelope or the
  `{{"__error__"}, {{nCode, cMsg, cSQL}}}` error shape, emits
  RowDescription with text-format OIDs and DataRow per row.
* typemap.go — pgTypeFor() picks INT4 / INT8 / NUMERIC / TEXT /
  DATE / TIMESTAMP / BOOL by sampling the first row's value type;
  encodeText() formats each cell, returning nil-slice for NULL
  (the PG length=-1 convention).
* errmap.go — sqlStateFor() maps FiveSql2 SQL_ERR_* codes to
  canonical PG SQLSTATEs (42601/42P01/42703/42804/23505/23514/
  23503/25P02/42501/02000/XX000).
* auth.go — trust mode in v1.0; password/MD5/SCRAM lands Phase 5
  but the dispatch sentinel is already in place.
* tls.go — upgradeToTLS stub for SSLRequest handling; the byte-
  ordering is already wired so Phase 6 just plugs in tls.Config.
* register.go — package init() registers pg_server_start /
  pg_server_stop HB_FUNCs. Importing the package (done from
  hbrtl/register.go via blank import) is enough to enable them.
* pgserver_test.go — unit tests for encodeText (numeric, string,
  NIL), pgTypeFor (OID dispatch), sqlStateFor (error mapping),
  commandTagFor (SELECT/INSERT/UPDATE/DELETE/BEGIN/COMMIT).

Other changes
-------------

* _FiveSql2/src/TSqlSession.prg — added PG_NEW_SESSION() factory
  used by the Go dispatcher to allocate a per-connection session
  bypassing the embedded process default.
* hbrtl/register.go — blank-import five/hbrtl/pgserver so its
  init() fires and the HB_FUNCs land in the global dynamic-func
  table for VM symbol lookup.
* go.mod / go.sum — github.com/jackc/pgx/v5 v5.9.2 (pgproto3
  subpackage). MIT license. Same library pgx itself uses, so
  protocol coverage matches the de-facto Go PG ecosystem.

Verification
------------

  $ pg_server_start(15432, "trust")     /* PRG one-liner */
  $ psql -h 127.0.0.1 -p 15432 -U fiveuser -c 'SELECT * FROM employees'
  → 3 rows rendered correctly by psql (ID as INT4, NAME as TEXT,
    SALARY as NUMERIC(10,2) with 2 decimal places)

All six release gates green:
  go test ./...               ✓ (incl. new hbrtl/pgserver tests)
  FiveSql2 SQL:1999 43/43     ✓
  Harbour compat 56/56        ✓
  std.ch 17/17                ✓
  FRB 7/7                     ✓
  examples 65/71              ✓ (unchanged baseline)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:40:32 +09:00