PostgreSQL clients (psql, pgx, DBeaver, Tableau, DataGrip,
pgAdmin) fire a barrage of catalog probes at connection time —
SELECT version(), SHOW server_version, SELECT FROM pg_namespace
/ pg_class / pg_type / pg_database / pg_settings. FiveSql2 can't
parse most of them. Without interception the BI tool either
errors out on connect or proceeds with a half-broken view of
the database (zero tables, no type info, no schema list). This
commit lands the minimum-viable catalog shim so the common
connect-and-list-tables flow succeeds.
Strategy
--------
Pattern-match catalog probes BEFORE handing the SQL to five_SQL.
Recognised shapes get synthesised result envelopes — same
`{ aFieldNames, aRows }` hbrt.Value shape the engine returns,
so the existing dispatchSimpleQuery / executePortal pipelines
stream them identically to a normal query.
Covered (v1.0)
--------------
* SET / RESET / DISCARD <name> → success, no-op
* SHOW <name> → single-row response
(server_version, server_encoding,
client_encoding, DateStyle,
transaction_isolation, etc.)
* SELECT version() / current_database() / current_schema() /
current_user / session_user / pg_backend_pid() → single-row
* SELECT … FROM pg_namespace → 2 rows (pg_catalog + public)
* SELECT … FROM pg_class → list of open workareas
(relkind='r', relnamespace=public)
* SELECT … FROM pg_attribute → empty (stub; column-shape
introspection deferred to v1.1)
* SELECT … FROM pg_type → 7 OIDs FiveSql2 actually emits
(bool, int4, int8, text, numeric,
date, timestamp)
* SELECT … FROM pg_database → 1 row, the connect-time db name
* SELECT … FROM pg_settings → name/setting pairs matching SHOW
* Anything else mentioning pg_catalog. / pg_<name> / information_schema.
→ empty result with generic field names (BI tool sees "0 rows" rather
than a parse error)
Deliberate non-goals
--------------------
* WHERE / JOIN evaluation — psql, pgx, DBeaver all filter
client-side on the rows we return. We send the whole
catalog and let them apply their predicates.
* pg_attribute introspection — would need to re-derive
column types from the open workarea + map back to PG OIDs.
Tracked as v1.1 work.
* Recursive CTE catalog queries (pgAdmin's tree builder uses
them) — too brittle to pattern-match. Falls through to
five_SQL where it errors loudly. pgAdmin's table-tree pane
will then show "0 tables" but the connection itself stays
alive.
Files
-----
hbrtl/pgserver/catalog.go (new, ~280 LOC)
catalogIntercept(sql) → (handled, value)
synthPgNamespace / synthPgClass / synthPgAttribute /
synthPgType / synthPgDatabase / synthPgSettings
simpleSelectFunction (version/current_*/pg_backend_pid)
showResponse (SHOW <name>)
hbrtl/pgserver/dispatch.go
dispatchSimpleQuery: catalogIntercept ahead of runSQL.
hbrtl/pgserver/extended.go
executePortal: same intercept, ahead of runSQL.
Verification
------------
psql against a running pgserver, with sslmode=require + MD5:
$ psql -c 'SELECT version()' -At
PostgreSQL 14.0 (FiveSql2) (FiveSql2 wire-compat shim)
$ psql -c 'SELECT * FROM pg_namespace' -At
11|pg_catalog|10
2200|public|10
$ psql -c 'SELECT * FROM pg_type' -At
16|bool|1
23|int4|4
20|int8|8
25|text|-1
1700|numeric|-1
1082|date|4
1114|timestamp|8
$ psql -l # \\l now works
데이터베이스 목록
oid | datname | datdba | 인코딩
-----+---------+--------+--------
1 | alice | 10 | 6
Integration script gates grew from 6/6 → 9/9:
PASS Catalog probe: SELECT version()
PASS Catalog probe: pg_namespace lists public + pg_catalog
PASS Catalog probe: SHOW server_version_num
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 9/9 ✓ (+3 from catalog stubs)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
206 lines
6.9 KiB
Bash
Executable File
206 lines
6.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# tests/pgserver/run.sh — integration harness for the pgserver wire
|
|
# layer. Builds a bootstrap PRG that starts the server, then drives
|
|
# it from a Go-side pgx client (located alongside this script).
|
|
#
|
|
# Verified scope (Phase 3):
|
|
# * CREATE TABLE / INSERT / UPDATE / DELETE over Simple Query
|
|
# * BEGIN / COMMIT / ROLLBACK from the wire
|
|
# * Two-connection cross-visibility on shared DBF
|
|
# * Per-session ROLLBACK doesn't affect other connection
|
|
#
|
|
# Known limitation (tracked for Phase 7):
|
|
# * ≥3 concurrent connections doing in-flight INSERT/SELECT in
|
|
# their own transactions can race at the hbrdd workarea layer
|
|
# — surfaces as one worker's just-inserted row missing from its
|
|
# own SELECT. Two-connection serial use, and N-connection use
|
|
# where each goroutine completes its txn before the next starts,
|
|
# are both reliable. Multi-way append-time WA arbitration is
|
|
# deferred until the audit's "WorkArea collision under multi-
|
|
# session" Top-Risk #2 fix lands.
|
|
|
|
set -e
|
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
FIVE="$ROOT/five"
|
|
PORT="${PGSERVER_TEST_PORT:-15432}"
|
|
|
|
if [ ! -x "$FIVE" ]; then
|
|
echo "five binary not found at $FIVE — run 'go build -o five ./cmd/five' first" >&2
|
|
exit 2
|
|
fi
|
|
if ! command -v psql >/dev/null 2>&1; then
|
|
echo "psql not in PATH — install PostgreSQL client tools to run this suite" >&2
|
|
exit 2
|
|
fi
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
# Bootstrap PRG — opens nothing, just stands up the server.
|
|
cat > "$work/boot.prg" <<EOF
|
|
PROCEDURE Main()
|
|
PG_SERVER_START( ":$PORT" )
|
|
RETURN
|
|
EOF
|
|
"$FIVE" build "$work/boot.prg" "$ROOT/_FiveSql2/src/"*.prg -o "$work/boot" >/dev/null 2>&1
|
|
|
|
"$work/boot" &
|
|
SERVER_PID=$!
|
|
sleep 1
|
|
trap "kill $SERVER_PID 2>/dev/null; rm -rf '$work'" EXIT
|
|
|
|
pass=0
|
|
total=0
|
|
ok() {
|
|
pass=$((pass+1))
|
|
total=$((total+1))
|
|
echo "PASS $1"
|
|
}
|
|
fail() {
|
|
total=$((total+1))
|
|
echo "FAIL $1"
|
|
echo "$2" | sed 's/^/ /'
|
|
}
|
|
|
|
# 1) Simple Query via psql.
|
|
out="$(psql "postgres://alice:any@127.0.0.1:$PORT/alice?sslmode=disable" \
|
|
-c "SELECT 1 AS one, 'hello' AS greet" -At 2>&1 || true)"
|
|
if echo "$out" | grep -q "1|hello"; then
|
|
ok "Simple Query: SELECT 1, 'hello'"
|
|
else
|
|
fail "Simple Query: SELECT 1, 'hello'" "$out"
|
|
fi
|
|
|
|
# 2) Multi-statement Simple Query — each ';'-separated stmt rolls
|
|
# through the engine independently. Verifies wire reuses one
|
|
# session across statements without bleed.
|
|
out="$(psql "postgres://alice:any@127.0.0.1:$PORT/alice?sslmode=disable" -At <<SQL 2>&1 || true
|
|
SELECT 'first';
|
|
SELECT 2 AS n;
|
|
SQL
|
|
)"
|
|
if echo "$out" | grep -q "first" && echo "$out" | grep -q "^2$"; then
|
|
ok "Multi-statement Simple Query"
|
|
else
|
|
fail "Multi-statement Simple Query" "$out"
|
|
fi
|
|
|
|
# Note on Extended Protocol coverage:
|
|
# psql can't drive raw Parse/Bind/Execute from -c invocations
|
|
# (PG's SQL-level PREPARE/EXECUTE is a separate feature that
|
|
# FiveSql2 doesn't parse). The Extended Protocol path is instead
|
|
# covered by hbrtl/pgserver/wire_test.go (Go unit) plus the
|
|
# pgx-driven manual sanity script in /tmp/pgs_test/. Adding a
|
|
# self-contained Go integration that bootstraps the server +
|
|
# drives pgx in one process is Phase 7 work.
|
|
|
|
# 3) Transaction control via simple query — BEGIN/COMMIT round-trip
|
|
# must leave ReadyForQuery in 'I' state so psql doesn't hang on
|
|
# the next command.
|
|
out="$(psql "postgres://alice:any@127.0.0.1:$PORT/alice?sslmode=disable" -At <<SQL 2>&1 || true
|
|
BEGIN;
|
|
SELECT 'in-txn';
|
|
COMMIT;
|
|
SELECT 'post-commit';
|
|
SQL
|
|
)"
|
|
if echo "$out" | grep -q "in-txn" && echo "$out" | grep -q "post-commit"; then
|
|
ok "Transaction control: BEGIN/COMMIT round-trip"
|
|
else
|
|
fail "Transaction control: BEGIN/COMMIT round-trip" "$out"
|
|
fi
|
|
|
|
# 4) MD5 authentication — kill the trust-mode server, restart with
|
|
# md5 + a known role, then verify both the rejection and success
|
|
# paths.
|
|
kill $SERVER_PID 2>/dev/null
|
|
wait 2>/dev/null
|
|
|
|
cat > "$work/auth.prg" <<EOF
|
|
PROCEDURE Main()
|
|
PG_ADD_ROLE( "alice", "swordfish" )
|
|
PG_SERVER_START( ":$PORT", "md5" )
|
|
RETURN
|
|
EOF
|
|
"$FIVE" build "$work/auth.prg" "$ROOT/_FiveSql2/src/"*.prg -o "$work/auth" >/dev/null 2>&1
|
|
"$work/auth" &
|
|
SERVER_PID=$!
|
|
sleep 1
|
|
trap "kill $SERVER_PID 2>/dev/null; rm -rf '$work'" EXIT
|
|
|
|
bad="$(PGPASSWORD=wrong psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=disable" \
|
|
-c "SELECT 1" 2>&1 | head -1 || true)"
|
|
if echo "$bad" | grep -qi "md5 authentication failed"; then
|
|
ok "MD5 auth: wrong password rejected"
|
|
else
|
|
fail "MD5 auth: wrong password rejected" "$bad"
|
|
fi
|
|
|
|
good="$(PGPASSWORD=swordfish psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=disable" \
|
|
-c "SELECT 'ok' AS x" -At 2>&1 || true)"
|
|
if echo "$good" | grep -q "^ok$"; then
|
|
ok "MD5 auth: correct password accepted"
|
|
else
|
|
fail "MD5 auth: correct password accepted" "$good"
|
|
fi
|
|
|
|
# 5) TLS — restart server with self-signed cert + allowlist and
|
|
# connect via psql sslmode=require.
|
|
kill $SERVER_PID 2>/dev/null
|
|
wait 2>/dev/null
|
|
|
|
cat > "$work/tls.prg" <<EOF
|
|
PROCEDURE Main()
|
|
PG_TLS_SELF_SIGNED( "$work/cert.pem", "$work/key.pem", "localhost" )
|
|
PG_ADD_ROLE( "alice", "swordfish" )
|
|
PG_ALLOW_IP( "127.0.0.1/32" )
|
|
PG_SERVER_START( ":$PORT", "md5" )
|
|
RETURN
|
|
EOF
|
|
"$FIVE" build "$work/tls.prg" "$ROOT/_FiveSql2/src/"*.prg -o "$work/tls" >/dev/null 2>&1
|
|
"$work/tls" &
|
|
SERVER_PID=$!
|
|
sleep 1
|
|
trap "kill $SERVER_PID 2>/dev/null; rm -rf '$work'" EXIT
|
|
|
|
tls_out="$(PGPASSWORD=swordfish psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=require" \
|
|
-c "SELECT 'tls-ok' AS x" -At 2>&1 || true)"
|
|
if echo "$tls_out" | grep -q "^tls-ok$"; then
|
|
ok "TLS handshake + MD5 auth via sslmode=require"
|
|
else
|
|
fail "TLS handshake + MD5 auth via sslmode=require" "$tls_out"
|
|
fi
|
|
|
|
# 6) pg_catalog stubs — BI-tool metadata probes must succeed
|
|
# (DBeaver / Tableau / pgAdmin send dozens of these on connect;
|
|
# we cover the common shapes so the connection doesn't error
|
|
# out before the tool gets to a real query).
|
|
ver_out="$(PGPASSWORD=swordfish psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=require" \
|
|
-c 'SELECT version()' -At 2>&1 || true)"
|
|
if echo "$ver_out" | grep -q 'PostgreSQL'; then
|
|
ok "Catalog probe: SELECT version()"
|
|
else
|
|
fail "Catalog probe: SELECT version()" "$ver_out"
|
|
fi
|
|
|
|
ns_out="$(PGPASSWORD=swordfish psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=require" \
|
|
-c 'SELECT * FROM pg_namespace' -At 2>&1 || true)"
|
|
if echo "$ns_out" | grep -q '|public|'; then
|
|
ok "Catalog probe: pg_namespace lists public + pg_catalog"
|
|
else
|
|
fail "Catalog probe: pg_namespace" "$ns_out"
|
|
fi
|
|
|
|
show_out="$(PGPASSWORD=swordfish psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=require" \
|
|
-c 'SHOW server_version_num' -At 2>&1 || true)"
|
|
if echo "$show_out" | grep -q '^140000$'; then
|
|
ok "Catalog probe: SHOW server_version_num"
|
|
else
|
|
fail "Catalog probe: SHOW server_version_num" "$show_out"
|
|
fi
|
|
|
|
echo "================================================================"
|
|
echo " pgserver integration: $pass / $total passed"
|
|
echo "================================================================"
|
|
[ $pass -eq $total ]
|