Commit Graph

4 Commits

Author SHA1 Message Date
d7a81af7db feat(pgserver): binary-format param decoding (Phase 4.1)
pgx defaults to binary wire format for INT2/INT4/INT8/FLOAT4/FLOAT8/
BOOL/NUMERIC/DATE/TIMESTAMP/TIMESTAMPTZ — Go's most-used PG driver
ships nearly every typed parameter as binary unless explicitly told
to use text mode. The Phase 3 implementation only decoded INT4/INT8/
BOOL, so any pgx call with a decimal price, a timestamp, or a date
was silently mis-quoted into the SQL stream.

Decoders now cover the seven additional OIDs. The interesting one is
NUMERIC: PG's wire format is base-10000 digit groups plus a separate
displayed-scale, so the decoder rebuilds the decimal string from
weight+sign+ndigits+digits[] without going through float (which would
lose precision for NUMERIC(38,*) values). Pinned by vectors covering
zero / positive / negative / fractional-only / NaN / multi-group
integer + fraction cases.

DATE / TIMESTAMP decoders assume integer_datetimes=on (which the
server advertises in ParameterStatus); the 8-byte microsecond delta
from the PG epoch (2000-01-01 UTC) is converted via Go's time.Time
machinery and re-emitted as a quoted SQL literal.

Text-format path also broadened: FLOAT4/FLOAT8/INT2 now transit
unquoted alongside INT4/INT8/BOOL/NUMERIC; the regression would have
been clients sending text-format floats getting them rewritten as
'1.5' (string literal) instead of 1.5 (numeric).

Verified: all 6 mandatory gates green (go test, SQL 43/43, compat
56/56, std.ch 17/17, FRB 7/7, pgserver 11/11). Five new decoder
tests pin each wire format against handcrafted PG payloads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:02:15 +09:00
e83787750a feat(pgserver): SCRAM-SHA-256 authentication (Phase 5.1)
PG14+ clients (libpq, pgx, JDBC) prefer SCRAM over MD5 when offered;
this lands the five-message exchange (SASL / SASLInitialResponse /
SASLContinue / SASLResponse / SASLFinal) so they get their preferred
path. MD5 stays as the universal fallback.

Storage stays plaintext in the in-memory role registry — per-auth we
generate a fresh salt + iter, derive SaltedPassword on the fly. Same
net security as the existing MD5 path, while matching wire output to
RFC 5802 byte for byte.

Critical detail: pgproto3's Backend multiplexes PasswordMessage,
SASLInitialResponse, and SASLResponse onto the same 'p' byte tag.
Without SetAuthType() the decoder picks PasswordMessage and the
handshake fails immediately. Switch state to AuthTypeSASL before
the client-first receive and AuthTypeSASLContinue before the
client-final receive.

Verified:
  * SCRAM math (PBKDF2 / HMAC / proof verify / server signature)
    via pinned unit test
  * Live psql round-trip — correct password accepted, wrong password
    rejected with proper SQLSTATE 28P01
  * All 6 mandatory gates green (go test, SQL 43/43, compat 56/56,
    std.ch 17/17, FRB 7/7, pgserver 11/11)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:24:34 +09:00
90eafcfc06 feat(pgserver): Phase 5 — password + MD5 authentication
Trust mode (v1.0 default) accepts anyone; that's fine for embedded
demo but unshipping a multi-client database without credentials
would be irresponsible. This commit adds two of libpq's three
standard auth flows. SCRAM-SHA-256 is Phase 5.1 — pgx/psql both
fall back to MD5 cleanly when the server advertises only md5, so
v1.0's functional coverage is complete with the pair landed here.

Auth subsystem
--------------

`hbrtl/pgserver/auth.go` adds:

* An in-memory role registry: `roleMap map[string]*role` guarded by
  sync.RWMutex. Reads (lookupRole) are hot-path during connection
  startup so the RWMutex lets multiple sessions auth in parallel
  without serialising through a plain Mutex.

* `AddRole(name, password)` / `RemoveRole(name)` Go API consumed
  by the new HB_FUNCs `PG_ADD_ROLE` / `PG_REMOVE_ROLE` (see
  register.go). Bootstrap PRG idiom:

      PG_ADD_ROLE("alice", "swordfish")
      PG_ADD_ROLE("bob",   "hunter2")
      PG_SERVER_START(":5432", "md5")

* `authPassword()` — cleartext PasswordMessage exchange. The wire
  payload is plain so intended for TLS-protected links only;
  Phase 6 ties the warning to actual TLS detection on the session.

* `authMD5()` — libpq's md5 challenge:

      server → AuthenticationMD5Password{salt: 4 random bytes}
      client → "md5" || md5_hex( md5_hex(password || user) || salt )

  We recompute the canonical hash from the stored plaintext and
  compare. md5Challenge() is exported for pinning by a Go unit
  test (vector cross-checked against libpq's fe-auth-md5.c).

Salt is sourced from crypto/rand on every challenge so replay
attacks against a captured wire trace can't reuse a prior hash.

Dispatch matrix (Config.AuthMode → flow):
  "" / "trust" → AuthenticationOk immediately, no lookup
  "password"   → authPassword()
  "md5"        → authMD5()
  anything else→ 28000 + connection close

Tests
-----

Unit (hbrtl/pgserver/pgserver_test.go):
  PASS  TestMD5Challenge           (vector + determinism + diff)
  PASS  TestRoleRegistry           (add/replace/remove/lookup)

Integration (tests/pgserver/run.sh):
  PASS  Simple Query: SELECT 1, 'hello'
  PASS  Multi-statement Simple Query
  PASS  Transaction control: BEGIN/COMMIT round-trip
  PASS  MD5 auth: wrong password rejected
  PASS  MD5 auth: correct password accepted

End-to-end matrix with real psql:
  wrong password   → "ERROR: md5 authentication failed for user 'alice'"
  correct password → SELECT returns row
  unknown user     → "ERROR: md5 authentication failed for user 'eve'"
  password mode    → cleartext exchange works equivalently

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 5/5    ✓ (up from 3/3 in Phase 4)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:01:30 +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