PostgreSQL protocol scheme for Rebol3
Current state of the scheme is under development and may be changed in any moment.
Open a connection using the postgres:// scheme.
pgsql: import %postgres.reb
; user/pass are taken from the URL, database is taken from the URL path:
pg: open postgres://postgres:password@localhost/postgres
res: write pg "SELECT current_user, current_database();"
probe res/rows
probe res/columns
print res/command-tag
close pg| Area | Support | Notes |
|---|---|---|
| Authentication | SCRAM, MD5, cleartext | Controlled by auth=scram,md5,cleartext allowlist. Cleartext is insecure unless used over TLS. |
| TLS | `sslmode=disable | prefer |
| Query protocol | Simple Query + Extended Query | Extended Query block forms: EXEC, PREPARE/EXECUTE/DEALLOCATE, CURSOR/FETCH/CLOSE-CURSOR. |
| Results | rows + column metadata | write returns a map! with rows, columns, command-tag, notices, runtime, etc. |
| Row shaping | `row=flat | block |
| Decoding | `decode=off | basic` (text format) |
| Async | queued per-connection | write block forms: ASYNC, ASYNC-STREAM (rows streaming + optional chunking). |
| Notifications | LISTEN/NOTIFY | Handlers invoked from IO/awake context; keep handlers fast. |
| Cancel | CancelRequest | pgsql/cancel pg cancels current inflight query when BackendKeyData is available. |
You can pass a small set of options using URL query params:
database=<name>: overrides the database from the URL pathauth=<list>: comma-separated list of allowed authentication methods:scram,md5,cleartextrow=<mode>: result row shaping:flat(default),block,mapdecode=<mode>: type decoding:off(default),basic(text format only)application_name=<string>: sets the Postgresapplication_namestartup parametersearch_path=<string>: runsSET search_path TO ...after connecting (treated as part ofopen)- Comma-separated schemas are supported (e.g.
public,extensions) $useris supported as a special token
- Comma-separated schemas are supported (e.g.
connect-timeout=<seconds>: handshake timeout (default: 10)query-timeout=<seconds>: timeout for blockingwrite(default: port spectimeout)log=<int>: setssystem/options/log/postgres(scheme-wide)trace=1|true|on: more verbose protocol logging (auth payloads are still redacted)sslmode=<mode>:disable(default),prefer,requirepreferwill try TLS and fall back to plaintext if the server refuses SSL.requirewill error if the server refuses SSL.preferis vulnerable to downgrade (an active attacker can block SSLRequest); userequireon untrusted networks.- Cleartext authentication over plaintext is insecure; use
sslmode=requireif you enableauth=cleartext. - Certificate verification knobs are not exposed by this scheme yet; treat TLS as encryption-in-transit, not identity verification.
Examples:
; force database without changing the URL path
pg: open postgres://postgres:password@localhost?database=postgres
; allow only SCRAM (disable MD5 + cleartext)
pg: open postgres://postgres:password@localhost/postgres?auth=scram
; return rows as maps and decode basic scalar types
pg: open postgres://postgres:password@localhost/postgres?row=map&decode=basic
; require TLS (fails if server refuses SSL/TLS)
pg: open postgres://postgres:password@localhost/postgres?sslmode=require
; prefer TLS (use TLS if available, otherwise plaintext)
pg: open postgres://postgres:password@localhost/postgres?sslmode=preferOn protocol/query errors write raises an error whose argument is a map! of server error fields
(including sql-state, message, detail, hint, etc. when present). The same map is also
available on the port as pg/extra/last-error.
err: try [write pg {SELECT * FROM nonexistingtable;}]
if error? :err [
probe err/arg1 ; map! with sql-state, message, ...
probe pg/extra/last-error
]write returns a map! with these keys:
rows: block of row values (currently text-decoded)columns: block of column metadata maps (name, type oid, etc.)command-tag: command completion tag (e.g."SELECT 1")notices: notices received during the queryruntime: collected runtime parameters (ParameterStatus)
The scheme supports a simple async API via write block forms. This is useful for event-driven programs where you donβt want write to block.
- Async query: returns immediately and calls completion callbacks later
done: none
on-done: func [res] [print ["DONE tag:" res/command-tag] done: true]
on-error: func [err] [print ["ERROR:" select err 'message] done: true]
; Enqueue and return immediately:
write pg [ASYNC "SELECT 1 AS x" :on-done :on-error]
; Let the port wake as IO progresses:
until [wait [pg 5] done]- Async streaming: delivers rows incrementally via
on-row(does not buffer all rows in memory)
rows-seen: 0
done: none
on-row: func [row] [rows-seen: rows-seen + 1] ; row shaping/decoding respects row= and decode=
on-done: func [res] [print ["DONE rows:" rows-seen] done: true]
on-error: func [err] [print ["ERROR:" select err 'message] done: true]
write pg [ASYNC-STREAM "SELECT generate_series(1, 250) AS x" :on-row :on-done :on-error]
until [wait [pg 10] done]Notes:
- Requests on a single connection are processed sequentially (Postgres protocol requires this). Async calls are queued and resolved in order.
- Callbacks are invoked from the portβs IO/awake processing; keep handlers fast and non-blocking.
- For chunked streaming (portal fetch), pass a final
max-rowsargument (e.g.50) to limit rows per fetch:write pg [ASYNC-STREAM "SELECT ..." :on-row :on-done :on-error 50]- Optional end-of-stream hook:
write pg [ASYNC-STREAM "SELECT ..." :on-row :on-done :on-error :on-complete 50]
The scheme handles Postgres NotificationResponse messages at any time and can dispatch them to user callbacks.
pgsql: import %postgres.reb
pg: open postgres://postgres:password@localhost/postgres
got: none
on-notify: func [evt][
print ["NOTIFY channel=" evt/channel " payload=" evt/payload]
got: evt
]
listen pg "demo_chan" :on-notify
; In another session (or same connection) send:
pgsql/notify pg "demo_chan" "hello"
until [wait [pg 10] got]
close pgIf the server provided BackendKeyData, you can cancel the currently inflight query:
pgsql: import %postgres.reb
pg: open postgres://postgres:password@localhost/postgres
done: none
on-done: func [res][done: 'ok]
on-err: func [err][done: err]
write pg [ASYNC "SELECT pg_sleep(10);" :on-done :on-err]
wait 0:0:0.2
pgsql/cancel pg
until [wait [pg 15] done]
close pgFor a fuller usage example (DDL/DML + error cases) see the test script: ci-test.r3.
- Single-connection concurrency: Postgres protocol is sequential per connection; async calls are queued and resolved in order.
- TLS verification: TLS is currently encryption-in-transit only; certificate/CA verification is not configurable yet.
- Logging:
log=<int>setssystem/options/log/postgres(scheme-wide/global).- Authentication payloads are redacted from logs;
traceincreases protocol verbosity but does not log secrets.
- Handler context: async callbacks and NOTIFY handlers run from the portβs IO/awake processing; keep them fast and non-blocking.
