Skip to main content

reddb_server/wire/postgres/
server.rs

1//! PostgreSQL wire-protocol listener (Phase 3.1 PG parity).
2//!
3//! Accepts TCP connections from PG-compatible clients, drives the startup
4//! handshake, and routes simple/extended-query frames into the existing
5//! `RedDBRuntime::execute_query` path. Results are adapted back into PG
6//! `RowDescription` + `DataRow` frames via `types::value_to_pg_wire_bytes`.
7
8use std::collections::HashMap;
9use std::sync::Arc;
10
11use tokio::io::{AsyncRead, AsyncWrite};
12use tokio::net::TcpListener;
13
14use super::catalog_views::translate_pg_catalog_query;
15use super::protocol::{
16    read_frame, read_startup, write_frame, write_raw_byte, BackendMessage, ColumnDescriptor,
17    DescribeTarget, FrontendMessage, PgWireError, TransactionStatus,
18};
19use super::types::{pg_param_to_param_value, value_to_pg_wire_bytes, PgOid};
20use crate::auth::Role;
21use crate::runtime::query_request::{
22    ParamValue, PreparedId, PreparedRegistry, QueryRequest, QueryRequestExecutor,
23};
24use crate::runtime::RedDBRuntime;
25use crate::storage::query::unified::{UnifiedRecord, UnifiedResult};
26use reddb_rql::ast::QueryExpr;
27use reddb_types::Value;
28
29/// Startup-tuned configuration for the PG wire listener.
30#[derive(Debug, Clone)]
31pub struct PgWireConfig {
32    /// TCP bind address ("host:port"). The caller is responsible for
33    /// keeping this disjoint from the native wire / gRPC / HTTP listeners.
34    pub bind_addr: String,
35    /// PG version string sent back in `ParameterStatus`. Many drivers
36    /// sniff this to enable/disable features. RedDB advertises a
37    /// recent-enough version to get the broadest client support.
38    pub server_version: String,
39    /// Optional TLS material. When set, an `SSLRequest` is answered with
40    /// `'S'` and the connection is upgraded via a rustls handshake before
41    /// the startup flow continues over the encrypted stream. The cert/key
42    /// surface is the *same* [`WireTlsConfig`] the native RedWire listener
43    /// uses — the two transports share one cert config rather than
44    /// inventing a parallel one. `None` leaves the historical behaviour:
45    /// `SSLRequest` is declined with `'N'` and clients continue in
46    /// plaintext.
47    pub tls: Option<crate::wire::tls::WireTlsConfig>,
48}
49
50/// The authenticated identity resolved during the PG-Wire startup
51/// handshake. Installed as the thread-local execution identity for the
52/// duration of each statement so every query dispatched over this
53/// connection flows through the same IAM/policy gates as native
54/// transports. `None` connections (loopback trust) run under the ambient
55/// embedded identity, preserving the pre-auth behaviour.
56struct PgAuthContext {
57    username: String,
58    role: Role,
59    tenant: Option<String>,
60    _lease: crate::auth::store::ConnectionLease,
61}
62
63#[derive(Debug, Clone)]
64struct PgPreparedStatement {
65    sql: String,
66    prepared_id: PreparedId,
67    param_type_oids: Vec<u32>,
68}
69
70#[derive(Debug, Clone)]
71struct PgPortal {
72    sql: String,
73    prepared_id: PreparedId,
74    params: Vec<ParamValue>,
75    #[allow(dead_code)]
76    result_format_codes: Vec<i16>,
77    row_description_sent: bool,
78    result: Option<crate::runtime::OwnedQueryResult>,
79    row_offset: usize,
80}
81
82impl Default for PgWireConfig {
83    fn default() -> Self {
84        Self {
85            bind_addr: "127.0.0.1:5432".to_string(),
86            server_version: "15.0 (RedDB 3.1)".to_string(),
87            tls: None,
88        }
89    }
90}
91
92/// Run a synchronous, possibly long-parking runtime call without
93/// monopolising the async worker thread.
94///
95/// `QUEUE READ … WAIT <budget>` parks the calling thread on a condvar
96/// for up to the wait budget. Doing that directly on a tokio worker
97/// holds the worker for the whole budget, which serialises every other
98/// connection's query behind the parked waiter — a producer's
99/// `QUEUE PUSH` could not run to notify the waiter, so an enqueue during
100/// a WAIT would never be delivered — and it stalls the runtime's timer
101/// driver. `block_in_place` hands the remaining tasks on this worker to
102/// a sibling worker for the duration while keeping the current OS thread
103/// — and therefore the per-connection thread-locals `execute_query`
104/// relies on (current tenant, connection id, transaction context) —
105/// intact, which `spawn_blocking` would not.
106///
107/// `block_in_place` panics on a current-thread runtime, so we fall back
108/// to a direct call there: a single-threaded runtime has no sibling
109/// worker to hand work to and nothing else to starve (e.g. the
110/// `#[tokio::test]` harnesses that drive one connection at a time).
111fn run_runtime_blocking<T>(f: impl FnOnce() -> T) -> T {
112    use tokio::runtime::{Handle, RuntimeFlavor};
113    match Handle::try_current().map(|h| h.runtime_flavor()) {
114        Ok(RuntimeFlavor::MultiThread) => tokio::task::block_in_place(f),
115        _ => f(),
116    }
117}
118
119fn pg_frame_requires_execution(frame: &FrontendMessage) -> bool {
120    matches!(
121        frame,
122        FrontendMessage::Query(_)
123            | FrontendMessage::Parse(_)
124            | FrontendMessage::Describe(_)
125            | FrontendMessage::Execute(_)
126    )
127}
128
129async fn acquire_pg_execution(
130    runtime: &RedDBRuntime,
131    latency_sensitive: bool,
132) -> Result<crate::transport::execution_admission::ExecutionPermit, &'static str> {
133    use crate::transport::execution_admission::ExecutionAdmissionError;
134
135    let admission = runtime.execution_admission();
136    match tokio::time::timeout(
137        runtime.execution_queue_timeout(),
138        admission.acquire_classified(latency_sensitive),
139    )
140    .await
141    {
142        Ok(Ok(permit)) => Ok(permit),
143        Ok(Err(ExecutionAdmissionError::QueueFull)) => {
144            Err("execution_queue_full: engine execution queue is full; retryable=true")
145        }
146        Ok(Err(ExecutionAdmissionError::Closed)) => {
147            Err("execution_admission_closed: engine execution admission is unavailable")
148        }
149        Err(_) => Err(
150            "execution_queue_timeout: deadline expired while waiting to execute; retryable=true",
151        ),
152    }
153}
154
155/// Holds the execution slot only until the response starts. PG handlers do
156/// all synchronous engine work before their first write; releasing at that
157/// boundary prevents a slow client from occupying engine capacity while rows
158/// drain through the socket.
159struct ExecutionResponseWriter<'a, S> {
160    inner: &'a mut S,
161    permit: Option<crate::transport::execution_admission::ExecutionPermit>,
162}
163
164impl<'a, S> ExecutionResponseWriter<'a, S> {
165    fn new(
166        inner: &'a mut S,
167        permit: Option<crate::transport::execution_admission::ExecutionPermit>,
168    ) -> Self {
169        Self { inner, permit }
170    }
171}
172
173impl<S: AsyncWrite + Unpin> AsyncWrite for ExecutionResponseWriter<'_, S> {
174    fn poll_write(
175        mut self: std::pin::Pin<&mut Self>,
176        cx: &mut std::task::Context<'_>,
177        buf: &[u8],
178    ) -> std::task::Poll<Result<usize, std::io::Error>> {
179        drop(self.permit.take());
180        std::pin::Pin::new(&mut *self.inner).poll_write(cx, buf)
181    }
182
183    fn poll_flush(
184        mut self: std::pin::Pin<&mut Self>,
185        cx: &mut std::task::Context<'_>,
186    ) -> std::task::Poll<Result<(), std::io::Error>> {
187        drop(self.permit.take());
188        std::pin::Pin::new(&mut *self.inner).poll_flush(cx)
189    }
190
191    fn poll_shutdown(
192        mut self: std::pin::Pin<&mut Self>,
193        cx: &mut std::task::Context<'_>,
194    ) -> std::task::Poll<Result<(), std::io::Error>> {
195        drop(self.permit.take());
196        std::pin::Pin::new(&mut *self.inner).poll_shutdown(cx)
197    }
198}
199
200/// Run `f` with the authenticated PG-Wire identity installed as the
201/// thread-local execution identity, restoring the prior state afterwards.
202///
203/// The runtime's query dispatch resolves `CURRENT_USER/ROLE/TENANT` and
204/// folds RLS/IAM policies from these thread-locals, so installing them
205/// here is what makes PG-Wire statements transport-invariant with the
206/// native wire. A `None` context leaves the ambient identity untouched.
207fn execute_with_pg_auth_context<T>(
208    auth_context: Option<&PgAuthContext>,
209    f: impl FnOnce() -> T,
210) -> T {
211    let Some(auth_context) = auth_context else {
212        return f();
213    };
214    let _guard = PgAuthContextGuard::install(auth_context);
215    f()
216}
217
218/// RAII guard that installs the PG-Wire auth identity + tenant on the
219/// current thread and restores the previous state on drop, so pooled
220/// runtime threads never leak an identity across statements.
221struct PgAuthContextGuard {
222    previous_principal: Option<crate::auth::UserId>,
223    previous_identity: Option<(String, Role)>,
224    previous_tenant: Option<String>,
225}
226
227impl PgAuthContextGuard {
228    fn install(auth_context: &PgAuthContext) -> Self {
229        let previous_tenant = crate::runtime::execution_context::current_tenant();
230        let previous_principal = crate::runtime::execution_context::current_auth_principal();
231        let previous_identity = crate::runtime::execution_context::current_auth_identity();
232        match &auth_context.tenant {
233            Some(tenant) => crate::runtime::execution_context::set_current_tenant(tenant.clone()),
234            None => crate::runtime::execution_context::clear_current_tenant(),
235        }
236        crate::runtime::execution_context::set_current_auth_principal(
237            crate::auth::UserId::from_parts(auth_context.tenant.as_deref(), &auth_context.username),
238            auth_context.role,
239        );
240        Self {
241            previous_principal,
242            previous_identity,
243            previous_tenant,
244        }
245    }
246}
247
248impl Drop for PgAuthContextGuard {
249    fn drop(&mut self) {
250        match (
251            self.previous_principal.take(),
252            self.previous_identity.take(),
253        ) {
254            (Some(principal), Some((_, role))) => {
255                crate::runtime::execution_context::set_current_auth_principal(principal, role)
256            }
257            _ => crate::runtime::execution_context::clear_current_auth_identity(),
258        }
259        match self.previous_tenant.take() {
260            Some(tenant) => crate::runtime::execution_context::set_current_tenant(tenant),
261            None => crate::runtime::execution_context::clear_current_tenant(),
262        }
263    }
264}
265
266/// Whether `bind_addr` ("host:port") resolves to a loopback host. Trust
267/// authentication survives only for loopback binds with no credentials
268/// configured; every other bind requires a verified password.
269fn is_loopback_bind(bind_addr: &str) -> bool {
270    crate::transport::is_loopback_bind(bind_addr)
271}
272
273/// Extract the cleartext password from a `PasswordMessage` payload (a
274/// single NUL-terminated C-string), returning `None` on malformed input.
275fn password_from_message(payload: &[u8]) -> Option<String> {
276    let nul = payload.iter().position(|&b| b == 0)?;
277    std::str::from_utf8(&payload[..nul])
278        .ok()
279        .map(|password| password.to_string())
280}
281
282/// Spawn the PG wire listener. Blocks until the listener errors out.
283/// Each connection is handled in its own tokio task.
284pub async fn start_pg_wire_listener(
285    config: PgWireConfig,
286    runtime: Arc<RedDBRuntime>,
287) -> Result<(), Box<dyn std::error::Error>> {
288    let listener = TcpListener::bind(&config.bind_addr).await?;
289    start_pg_wire_listener_on(listener, config, runtime).await
290}
291
292/// Serve PostgreSQL wire traffic on a listener bound during node bootstrap.
293pub async fn start_pg_wire_listener_on(
294    listener: TcpListener,
295    config: PgWireConfig,
296    runtime: Arc<RedDBRuntime>,
297) -> Result<(), Box<dyn std::error::Error>> {
298    // Posture: with no auth store enabled, PG-Wire falls back to trust
299    // auth for loopback binds only (see `authenticate_startup`). Surface a
300    // startup warning whenever that fallback is active so an operator never
301    // exposes an unauthenticated listener without noticing.
302    let trust_active = runtime
303        .auth_store()
304        .map(|store| !store.is_enabled())
305        .unwrap_or(true)
306        && is_loopback_bind(&config.bind_addr);
307    if trust_active {
308        tracing::warn!(
309            transport = "pg-wire",
310            bind = %config.bind_addr,
311            "PG-Wire trust authentication is active (no auth store configured); \
312             loopback-only. Configure auth before exposing this listener."
313        );
314    }
315    // Build the shared TLS acceptor once, up front, so a bad cert/key
316    // fails the whole listener rather than every individual connection.
317    // The acceptor is cheap to clone (an `Arc` internally) and is handed
318    // to each connection so `SSLRequest` can be answered with `'S'` and
319    // the rustls handshake performed before the startup flow resumes.
320    let tls_acceptor = match config.tls.as_ref() {
321        Some(tls_cfg) => Some(crate::wire::tls::build_tls_acceptor(tls_cfg)?),
322        None => None,
323    };
324    tracing::info!(
325        transport = "pg-wire",
326        bind = %config.bind_addr,
327        tls = tls_acceptor.is_some(),
328        "listener online"
329    );
330    let cfg = Arc::new(config);
331    let admission = crate::wire::admission::ConnectionAdmission::new("pg-wire");
332    loop {
333        let (stream, peer) = listener.accept().await?;
334        // Bound concurrent sessions. Without this the accept loop spawned a
335        // task per connection with no ceiling, so an unauthenticated peer
336        // could exhaust descriptors before presenting a credential.
337        let Some(permit) = admission.try_admit() else {
338            continue;
339        };
340        // Small result/control frames must not wait for a client's delayed ACK
341        // before the next frame (notably ReadyForQuery) can leave the server.
342        if let Err(error) = stream.set_nodelay(true) {
343            tracing::warn!(transport = "pg-wire", peer = %peer, err = %error,
344                "failed to configure connection");
345            continue;
346        }
347        let rt = Arc::clone(&runtime);
348        let cfg = Arc::clone(&cfg);
349        let acceptor = tls_acceptor.clone();
350        let peer_str = peer.to_string();
351        tokio::spawn(async move {
352            let _permit = permit;
353            if let Err(e) = handle_connection(stream, rt, cfg, acceptor).await {
354                tracing::warn!(
355                    transport = "pg-wire",
356                    peer = %peer_str,
357                    err = %e,
358                    "connection failed"
359                );
360            }
361        });
362    }
363}
364
365/// Drive one connection's lifetime: SSL negotiation → startup →
366/// authentication → query loop.
367///
368/// The first frame may be `SSLRequest` / `GSSENCRequest` (pre-auth
369/// negotiation) or a plain `Startup`. When a TLS acceptor is present an
370/// `SSLRequest` is answered with `'S'` and the socket is upgraded via a
371/// rustls handshake; the rest of the session (a fresh `Startup`,
372/// authentication, and the query loop) then runs over the encrypted
373/// stream. Without an acceptor — or for `GSSENCRequest`, which we never
374/// support — the request is declined with `'N'` and the client continues
375/// in plaintext on the same socket.
376pub(crate) async fn handle_connection<S>(
377    mut stream: S,
378    runtime: Arc<RedDBRuntime>,
379    config: Arc<PgWireConfig>,
380    tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
381) -> Result<(), PgWireError>
382where
383    S: AsyncRead + AsyncWrite + Unpin + Send,
384{
385    // Bound the whole pre-session negotiation. `read_startup` and the rustls
386    // handshake both block on the peer, and neither had a deadline: a client
387    // that connected and sent nothing (or stalled mid-handshake) pinned a
388    // task and a connection slot indefinitely, before any credential was
389    // presented. The loop below can iterate (SSLRequest/GSSENCRequest are
390    // declined and the client retries), so each read is bounded rather than
391    // the loop as a whole.
392    loop {
393        let startup = crate::wire::admission::with_handshake_deadline(read_startup(&mut stream))
394            .await
395            .map_err(PgWireError::from)??;
396        match startup {
397            FrontendMessage::SslRequest => {
398                if let Some(acceptor) = tls_acceptor.as_ref() {
399                    // 'S' = SSL supported. Perform the rustls handshake and
400                    // hand the encrypted stream to the session driver.
401                    write_raw_byte(&mut stream, b'S').await?;
402                    let tls_stream =
403                        crate::wire::admission::with_handshake_deadline(acceptor.accept(stream))
404                            .await??;
405                    return serve_session_after_tls(tls_stream, runtime, config).await;
406                }
407                // 'N' = not supported — client continues in plaintext and
408                // re-sends a normal Startup on the same socket.
409                write_raw_byte(&mut stream, b'N').await?;
410                continue;
411            }
412            FrontendMessage::GssEncRequest => {
413                // GSSAPI encryption is never offered; decline and let the
414                // client retry with SSLRequest or plaintext Startup.
415                write_raw_byte(&mut stream, b'N').await?;
416                continue;
417            }
418            FrontendMessage::Startup(params) => {
419                return run_pg_session(stream, runtime, config, false, params).await;
420            }
421            FrontendMessage::Unknown { .. } => {
422                // CancelRequest: no response expected; drop the socket.
423                return Ok(());
424            }
425            other => {
426                return Err(PgWireError::Protocol(format!(
427                    "unexpected startup frame: {other:?}"
428                )));
429            }
430        }
431    }
432}
433
434/// Continue the startup flow after a successful TLS upgrade. The client
435/// now sends a fresh `Startup` over the encrypted stream; a second
436/// `SSLRequest` / `GSSENCRequest` cannot be honoured (no nested
437/// encryption) and is declined with `'N'`.
438async fn serve_session_after_tls<S>(
439    mut stream: tokio_rustls::server::TlsStream<S>,
440    runtime: Arc<RedDBRuntime>,
441    config: Arc<PgWireConfig>,
442) -> Result<(), PgWireError>
443where
444    S: AsyncRead + AsyncWrite + Unpin + Send,
445{
446    loop {
447        match read_startup(&mut stream).await? {
448            FrontendMessage::SslRequest | FrontendMessage::GssEncRequest => {
449                write_raw_byte(&mut stream, b'N').await?;
450                continue;
451            }
452            FrontendMessage::Startup(params) => {
453                return run_pg_session(stream, runtime, config, true, params).await;
454            }
455            FrontendMessage::Unknown { .. } => return Ok(()),
456            other => {
457                return Err(PgWireError::Protocol(format!(
458                    "unexpected startup frame: {other:?}"
459                )));
460            }
461        }
462    }
463}
464
465/// Authenticate a `Startup` and drive the query loop over `stream`.
466/// `over_tls` records whether the transport is encrypted, which gates the
467/// cleartext-password posture in [`authenticate_startup`].
468async fn run_pg_session<S>(
469    mut stream: S,
470    runtime: Arc<RedDBRuntime>,
471    config: Arc<PgWireConfig>,
472    over_tls: bool,
473    params: super::protocol::StartupParams,
474) -> Result<(), PgWireError>
475where
476    S: AsyncRead + AsyncWrite + Unpin + Send,
477{
478    let auth_context =
479        match authenticate_startup(&mut stream, &runtime, &config, over_tls, &params).await? {
480            Some(context) => context,
481            // Authentication failed: the error frame was already sent and
482            // the socket must close.
483            None => return Ok(()),
484        };
485
486    let mut prepared: HashMap<String, PgPreparedStatement> = HashMap::new();
487    let mut portals: HashMap<String, PgPortal> = HashMap::new();
488    let prepared_registry = PreparedRegistry::new();
489
490    // Main query loop.
491    loop {
492        let frame = match read_frame(&mut stream).await {
493            Ok(f) => f,
494            Err(PgWireError::Eof) => return Ok(()),
495            Err(e) => return Err(e),
496        };
497
498        // PG sessions are serial, but every session competes for the same
499        // runtime-wide engine budget as HTTP, gRPC and RedWire. Admission is
500        // taken before any parser/planner/storage work. The response writer
501        // releases it before the first socket write, so a slow client cannot
502        // occupy engine capacity while rows drain.
503        let mut execution_permit = if pg_frame_requires_execution(&frame) {
504            let latency_sensitive = matches!(
505                &frame,
506                FrontendMessage::Query(sql)
507                    if crate::server::is_trivial_scalar_sql(sql)
508            );
509            match acquire_pg_execution(&runtime, latency_sensitive).await {
510                Ok(permit) => Some(permit),
511                Err(message) => {
512                    // SQLSTATE 53000 = insufficient_resources. Keep the stable
513                    // RedDB reason code in the message for clients that expose
514                    // only the PostgreSQL ErrorResponse text.
515                    send_error(&mut stream, "53000", message).await?;
516                    write_frame(
517                        &mut stream,
518                        &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
519                    )
520                    .await?;
521                    continue;
522                }
523            }
524        } else {
525            None
526        };
527
528        match frame {
529            FrontendMessage::Query(sql) => {
530                let mut response =
531                    ExecutionResponseWriter::new(&mut stream, execution_permit.take());
532                handle_simple_query(&mut response, &runtime, auth_context.as_ref(), &sql).await?;
533            }
534            FrontendMessage::Parse(msg) => {
535                let mut response =
536                    ExecutionResponseWriter::new(&mut stream, execution_permit.take());
537                handle_parse(
538                    &mut response,
539                    &runtime,
540                    &prepared_registry,
541                    &mut prepared,
542                    &portals,
543                    msg,
544                )
545                .await?;
546            }
547            FrontendMessage::Bind(msg) => {
548                handle_bind(
549                    &mut stream,
550                    &prepared_registry,
551                    &prepared,
552                    &mut portals,
553                    msg,
554                )
555                .await?;
556            }
557            FrontendMessage::Describe(msg) => {
558                let mut response =
559                    ExecutionResponseWriter::new(&mut stream, execution_permit.take());
560                handle_describe(
561                    &mut response,
562                    &runtime,
563                    auth_context.as_ref(),
564                    &prepared_registry,
565                    &prepared,
566                    &mut portals,
567                    msg,
568                )
569                .await?;
570            }
571            FrontendMessage::Execute(msg) => {
572                let mut response =
573                    ExecutionResponseWriter::new(&mut stream, execution_permit.take());
574                handle_execute(
575                    &mut response,
576                    &runtime,
577                    auth_context.as_ref(),
578                    &prepared_registry,
579                    &mut portals,
580                    msg,
581                )
582                .await?;
583            }
584            FrontendMessage::Close(msg) => {
585                handle_close(
586                    &mut stream,
587                    &prepared_registry,
588                    &mut prepared,
589                    &mut portals,
590                    msg,
591                )
592                .await?;
593            }
594            FrontendMessage::Terminate => return Ok(()),
595            FrontendMessage::Flush => {
596                // Frames are written immediately; no additional marker is
597                // needed. ReadyForQuery belongs to Sync, not Flush.
598                continue;
599            }
600            FrontendMessage::Sync => {
601                write_frame(
602                    &mut stream,
603                    &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
604                )
605                .await?;
606            }
607            FrontendMessage::PasswordMessage(_) => {
608                // Should only arrive during auth. Ignore post-auth.
609                continue;
610            }
611            FrontendMessage::Unknown { tag, .. } => {
612                send_error(
613                    &mut stream,
614                    "0A000",
615                    &format!("unsupported frame tag 0x{tag:02x}"),
616                )
617                .await?;
618                write_frame(
619                    &mut stream,
620                    &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
621                )
622                .await?;
623            }
624            other => {
625                send_error(
626                    &mut stream,
627                    "0A000",
628                    &format!("unsupported frame {other:?}"),
629                )
630                .await?;
631                write_frame(
632                    &mut stream,
633                    &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
634                )
635                .await?;
636            }
637        }
638    }
639}
640
641async fn handle_parse<S>(
642    stream: &mut S,
643    runtime: &RedDBRuntime,
644    prepared_registry: &PreparedRegistry,
645    prepared: &mut HashMap<String, PgPreparedStatement>,
646    portals: &HashMap<String, PgPortal>,
647    msg: super::protocol::ParseMessage,
648) -> Result<(), PgWireError>
649where
650    S: AsyncWrite + Unpin,
651{
652    let inferred_param_type_oids = infer_pg_cast_param_type_oids(&msg.query);
653    let sql = rewrite_pg_parameter_casts(&msg.query);
654    let prepared_query = match prepared_registry.prepare(runtime, &sql) {
655        Ok(prepared_query) => prepared_query,
656        Err(err) => {
657            send_error(stream, "42601", &err.to_string()).await?;
658            return Ok(());
659        }
660    };
661    let mut param_type_oids = msg.param_type_oids;
662    if param_type_oids.is_empty() {
663        param_type_oids.resize(prepared_query.parameter_count, PgOid::Unknown.as_u32());
664    }
665    for (idx, oid) in inferred_param_type_oids {
666        if idx >= param_type_oids.len() {
667            param_type_oids.resize(idx + 1, PgOid::Unknown.as_u32());
668        }
669        if param_type_oids[idx] == PgOid::Unknown.as_u32() {
670            param_type_oids[idx] = oid;
671        }
672    }
673    let replaced = prepared.insert(
674        msg.statement,
675        PgPreparedStatement {
676            sql,
677            prepared_id: prepared_query.id,
678            param_type_oids,
679        },
680    );
681    if let Some(replaced) = replaced {
682        release_prepared_if_unreferenced(
683            prepared_registry,
684            replaced.prepared_id,
685            prepared,
686            portals,
687        );
688    }
689    write_frame(stream, &BackendMessage::ParseComplete).await
690}
691
692async fn handle_bind<S>(
693    stream: &mut S,
694    prepared_registry: &PreparedRegistry,
695    prepared: &HashMap<String, PgPreparedStatement>,
696    portals: &mut HashMap<String, PgPortal>,
697    msg: super::protocol::BindMessage,
698) -> Result<(), PgWireError>
699where
700    S: AsyncWrite + Unpin,
701{
702    let Some(stmt) = prepared.get(&msg.statement) else {
703        send_error(
704            stream,
705            "26000",
706            &format!("prepared statement {:?} does not exist", msg.statement),
707        )
708        .await?;
709        return Ok(());
710    };
711    let params = match bind_pg_params(stmt, &msg) {
712        Ok(params) => params,
713        Err(err) => {
714            send_error(stream, "22023", &err).await?;
715            return Ok(());
716        }
717    };
718    let replaced = portals.insert(
719        msg.portal,
720        PgPortal {
721            sql: stmt.sql.clone(),
722            prepared_id: stmt.prepared_id,
723            params,
724            result_format_codes: msg.result_format_codes,
725            row_description_sent: false,
726            result: None,
727            row_offset: 0,
728        },
729    );
730    if let Some(replaced) = replaced {
731        release_prepared_if_unreferenced(
732            prepared_registry,
733            replaced.prepared_id,
734            prepared,
735            portals,
736        );
737    }
738    write_frame(stream, &BackendMessage::BindComplete).await
739}
740
741async fn handle_describe<S>(
742    stream: &mut S,
743    runtime: &RedDBRuntime,
744    auth_context: Option<&PgAuthContext>,
745    prepared_registry: &PreparedRegistry,
746    prepared: &HashMap<String, PgPreparedStatement>,
747    portals: &mut HashMap<String, PgPortal>,
748    msg: super::protocol::DescribeMessage,
749) -> Result<(), PgWireError>
750where
751    S: AsyncWrite + Unpin,
752{
753    match msg.target {
754        DescribeTarget::Statement => {
755            let Some(stmt) = prepared.get(&msg.name) else {
756                send_error(
757                    stream,
758                    "26000",
759                    &format!("prepared statement {:?} does not exist", msg.name),
760                )
761                .await?;
762                return Ok(());
763            };
764            write_frame(
765                stream,
766                &BackendMessage::ParameterDescription(stmt.param_type_oids.clone()),
767            )
768            .await?;
769            if is_ask_query(&stmt.sql) {
770                emit_ask_row_description(stream).await
771            } else {
772                write_frame(stream, &BackendMessage::NoData).await
773            }
774        }
775        DescribeTarget::Portal => {
776            let Some(portal) = portals.get_mut(&msg.name) else {
777                send_error(
778                    stream,
779                    "34000",
780                    &format!("portal {:?} does not exist", msg.name),
781                )
782                .await?;
783                return Ok(());
784            };
785            if is_ask_query(&portal.sql) {
786                emit_ask_row_description(stream).await?;
787                portal.row_description_sent = true;
788                Ok(())
789            } else if is_row_returning_query(&portal.sql) {
790                let result = match execute_pg_query_result(
791                    runtime,
792                    auth_context,
793                    prepared_registry,
794                    portal.prepared_id,
795                    &portal.sql,
796                    &portal.params,
797                ) {
798                    Ok(result) => result,
799                    Err(err) => {
800                        let code = classify_sqlstate(&err);
801                        send_error(stream, code, &err).await?;
802                        return Ok(());
803                    }
804                };
805                emit_row_description_for_result(stream, &result).await?;
806                portal.row_description_sent = true;
807                portal.result = Some(result);
808                portal.row_offset = 0;
809                Ok(())
810            } else {
811                write_frame(stream, &BackendMessage::NoData).await
812            }
813        }
814    }
815}
816
817async fn handle_execute<S>(
818    stream: &mut S,
819    runtime: &RedDBRuntime,
820    auth_context: Option<&PgAuthContext>,
821    prepared_registry: &PreparedRegistry,
822    portals: &mut HashMap<String, PgPortal>,
823    msg: super::protocol::ExecuteMessage,
824) -> Result<(), PgWireError>
825where
826    S: AsyncWrite + Unpin,
827{
828    let Some(portal) = portals.get_mut(&msg.portal) else {
829        send_error(
830            stream,
831            "34000",
832            &format!("portal {:?} does not exist", msg.portal),
833        )
834        .await?;
835        return Ok(());
836    };
837    match prepared_registry.ddl_epoch(portal.prepared_id) {
838        Some(epoch) if epoch != runtime.ddl_epoch() => {
839            send_error(stream, "XX000", "prepared_needs_replan").await?;
840            return Ok(());
841        }
842        Some(_) => {}
843        None => {
844            send_error(stream, "26000", "prepared statement does not exist").await?;
845            return Ok(());
846        }
847    }
848    if portal.result.is_none() {
849        match execute_pg_query_result(
850            runtime,
851            auth_context,
852            prepared_registry,
853            portal.prepared_id,
854            &portal.sql,
855            &portal.params,
856        ) {
857            Ok(result) => {
858                portal.result = Some(result);
859                portal.row_offset = 0;
860            }
861            Err(err) => {
862                let code = classify_sqlstate(&err);
863                send_error(stream, code, &err).await?;
864                return Ok(());
865            }
866        }
867    }
868
869    let result = portal.result.as_ref().expect("portal result populated");
870    match emit_portal_execution(
871        stream,
872        result,
873        portal.row_description_sent,
874        portal.row_offset,
875        msg.max_rows,
876    )
877    .await
878    {
879        Ok(PortalExecution::Complete) => {
880            portal.result = None;
881            portal.row_offset = 0;
882            portal.row_description_sent = false;
883            Ok(())
884        }
885        Ok(PortalExecution::Suspended { next_row_offset }) => {
886            portal.row_offset = next_row_offset;
887            portal.row_description_sent = true;
888            Ok(())
889        }
890        Err(err) => {
891            let code = classify_sqlstate(&err);
892            send_error(stream, code, &err).await
893        }
894    }
895}
896
897async fn handle_close<S>(
898    stream: &mut S,
899    prepared_registry: &PreparedRegistry,
900    prepared: &mut HashMap<String, PgPreparedStatement>,
901    portals: &mut HashMap<String, PgPortal>,
902    msg: super::protocol::CloseMessage,
903) -> Result<(), PgWireError>
904where
905    S: AsyncWrite + Unpin,
906{
907    let prepared_id = match msg.target {
908        DescribeTarget::Statement => prepared.remove(&msg.name).map(|stmt| stmt.prepared_id),
909        DescribeTarget::Portal => portals.remove(&msg.name).map(|portal| portal.prepared_id),
910    };
911    if let Some(prepared_id) = prepared_id {
912        release_prepared_if_unreferenced(prepared_registry, prepared_id, prepared, portals);
913    }
914    write_frame(stream, &BackendMessage::CloseComplete).await
915}
916
917fn release_prepared_if_unreferenced(
918    prepared_registry: &PreparedRegistry,
919    prepared_id: PreparedId,
920    prepared: &HashMap<String, PgPreparedStatement>,
921    portals: &HashMap<String, PgPortal>,
922) {
923    let statement_references_id = prepared
924        .values()
925        .any(|statement| statement.prepared_id == prepared_id);
926    let portal_references_id = portals
927        .values()
928        .any(|portal| portal.prepared_id == prepared_id);
929    if !statement_references_id && !portal_references_id {
930        prepared_registry.release(prepared_id);
931    }
932}
933
934fn bind_pg_params(
935    stmt: &PgPreparedStatement,
936    msg: &super::protocol::BindMessage,
937) -> Result<Vec<ParamValue>, String> {
938    if !matches!(msg.param_format_codes.len(), 0 | 1)
939        && msg.param_format_codes.len() != msg.params.len()
940    {
941        return Err("Bind format count must be 0, 1, or match parameter count".to_string());
942    }
943    msg.params
944        .iter()
945        .enumerate()
946        .map(|(idx, param)| {
947            let oid = stmt
948                .param_type_oids
949                .get(idx)
950                .copied()
951                .unwrap_or(PgOid::Unknown.as_u32());
952            let format_code = match msg.param_format_codes.as_slice() {
953                [] => 0,
954                [format] => *format,
955                formats => formats[idx],
956            };
957            pg_param_to_param_value(oid, format_code, param.as_deref())
958        })
959        .collect()
960}
961
962fn execute_pg_query_result(
963    runtime: &RedDBRuntime,
964    auth_context: Option<&PgAuthContext>,
965    prepared_registry: &PreparedRegistry,
966    prepared_id: PreparedId,
967    sql: &str,
968    params: &[ParamValue],
969) -> Result<crate::runtime::OwnedQueryResult, String> {
970    let executor = QueryRequestExecutor::new(runtime, prepared_registry);
971    let bound = executor
972        .bind(QueryRequest::prepared(prepared_id, params.to_vec()))
973        .map_err(|err| err.to_string())?;
974    if pg_scalar_select_param_index(sql).is_some() {
975        let values = params.iter().cloned().map(Value::from).collect::<Vec<_>>();
976        if let Some(result) = try_execute_pg_scalar_select(sql, &values) {
977            return runtime
978                .own_query_result(result)
979                .map_err(|error| error.to_string());
980        }
981    }
982    if params.is_empty() {
983        return match translate_pg_catalog_query(runtime, sql) {
984            Ok(Some(result)) => runtime
985                .own_query_result(crate::runtime::RuntimeQueryResult {
986                    query: sql.to_string(),
987                    mode: reddb_rql::modes::QueryMode::Sql,
988                    statement: "select",
989                    engine: "pg-catalog",
990                    result,
991                    affected_rows: 0,
992                    statement_type: "select",
993                    bookmark: None,
994                    notice: None,
995                })
996                .map_err(|error| error.to_string()),
997            Ok(None) => run_runtime_blocking(|| {
998                execute_with_pg_auth_context(auth_context, || {
999                    // Utility statements use the full dispatcher after registry validation.
1000                    // Their zero-parameter SQL can be reparsed without losing bindings;
1001                    // data queries retain the cached prepared shape on repeated Execute.
1002                    if matches!(
1003                        bound.prepared_expr(),
1004                        Some(
1005                            QueryExpr::Function(_)
1006                                | QueryExpr::Table(_)
1007                                | QueryExpr::Graph(_)
1008                                | QueryExpr::Path(_)
1009                                | QueryExpr::Join(_)
1010                                | QueryExpr::Vector(_)
1011                                | QueryExpr::Hybrid(_)
1012                                | QueryExpr::Insert(_)
1013                                | QueryExpr::Update(_)
1014                                | QueryExpr::Delete(_)
1015                                | QueryExpr::QueueCommand(_)
1016                                | QueryExpr::KvCommand(_)
1017                                | QueryExpr::SearchCommand(_)
1018                                | QueryExpr::Ask(_)
1019                        )
1020                    ) {
1021                        executor.execute_bound(bound)
1022                    } else {
1023                        runtime
1024                            .execute_query(sql)
1025                            .and_then(|result| runtime.own_query_result(result))
1026                    }
1027                })
1028            })
1029            .map_err(|err| err.to_string()),
1030            Err(err) => Err(err.to_string()),
1031        };
1032    }
1033
1034    run_runtime_blocking(|| {
1035        execute_with_pg_auth_context(auth_context, || executor.execute_bound(bound))
1036    })
1037    .map_err(|err| err.to_string())
1038}
1039
1040fn try_execute_pg_scalar_select(
1041    sql: &str,
1042    params: &[Value],
1043) -> Option<crate::runtime::RuntimeQueryResult> {
1044    let index = pg_scalar_select_param_index(sql)?;
1045    let value = params.get(index)?.clone();
1046    let mut result = UnifiedResult::with_columns(vec!["?column?".to_string()]);
1047    let mut record = UnifiedRecord::new();
1048    record.set("?column?", value);
1049    result.push(record);
1050    Some(crate::runtime::RuntimeQueryResult {
1051        query: sql.to_string(),
1052        mode: reddb_rql::modes::QueryMode::Sql,
1053        statement: "select",
1054        engine: "pg-wire",
1055        result,
1056        affected_rows: 0,
1057        statement_type: "select",
1058        bookmark: None,
1059        notice: None,
1060    })
1061}
1062
1063fn pg_scalar_select_param_index(sql: &str) -> Option<usize> {
1064    let trimmed = sql.trim().trim_end_matches(';').trim();
1065    let lower = trimmed.to_ascii_lowercase();
1066    let body = lower.strip_prefix("select ")?;
1067    let param = if let Some(inner) = body.strip_prefix("cast(") {
1068        let end = inner.find(" as ")?;
1069        &inner[..end]
1070    } else {
1071        body.split_whitespace().next()?
1072    };
1073    let digits = param.strip_prefix('$')?;
1074    let n = digits.parse::<usize>().ok()?;
1075    n.checked_sub(1)
1076}
1077
1078fn rewrite_pg_parameter_casts(sql: &str) -> String {
1079    let mut out = String::with_capacity(sql.len());
1080    let bytes = sql.as_bytes();
1081    let mut cursor = 0;
1082    let mut pos = 0;
1083    while pos < bytes.len() {
1084        if bytes[pos] != b'$' {
1085            pos += 1;
1086            continue;
1087        }
1088        let param_start = pos;
1089        pos += 1;
1090        let digits_start = pos;
1091        while pos < bytes.len() && bytes[pos].is_ascii_digit() {
1092            pos += 1;
1093        }
1094        if digits_start == pos {
1095            continue;
1096        }
1097        if pos + 2 <= bytes.len() && &bytes[pos..pos + 2] == b"::" {
1098            let param_end = pos;
1099            pos += 2;
1100            let type_start = pos;
1101            while pos < bytes.len()
1102                && (bytes[pos].is_ascii_alphanumeric() || matches!(bytes[pos], b'_' | b'.'))
1103            {
1104                pos += 1;
1105            }
1106            if type_start != pos {
1107                out.push_str(&sql[cursor..param_start]);
1108                out.push_str(&sql[param_start..param_end]);
1109                cursor = pos;
1110                continue;
1111            }
1112        }
1113    }
1114    out.push_str(&sql[cursor..]);
1115    out
1116}
1117
1118fn infer_pg_cast_param_type_oids(sql: &str) -> Vec<(usize, u32)> {
1119    let mut out = Vec::new();
1120    let bytes = sql.as_bytes();
1121    let mut pos = 0;
1122    while pos < bytes.len() {
1123        if bytes[pos] != b'$' {
1124            pos += 1;
1125            continue;
1126        }
1127        pos += 1;
1128        let digits_start = pos;
1129        while pos < bytes.len() && bytes[pos].is_ascii_digit() {
1130            pos += 1;
1131        }
1132        if digits_start == pos {
1133            continue;
1134        }
1135        let Some(param_index) = sql[digits_start..pos]
1136            .parse::<usize>()
1137            .ok()
1138            .and_then(|idx| idx.checked_sub(1))
1139        else {
1140            continue;
1141        };
1142        if pos + 2 > bytes.len() || &bytes[pos..pos + 2] != b"::" {
1143            continue;
1144        }
1145        pos += 2;
1146        let type_start = pos;
1147        while pos < bytes.len()
1148            && (bytes[pos].is_ascii_alphanumeric() || matches!(bytes[pos], b'_' | b'.'))
1149        {
1150            pos += 1;
1151        }
1152        if type_start == pos {
1153            continue;
1154        }
1155        if let Some(oid) = pg_cast_type_oid(&sql[type_start..pos]) {
1156            out.push((param_index, oid));
1157        }
1158    }
1159    out
1160}
1161
1162fn pg_cast_type_oid(ty: &str) -> Option<u32> {
1163    let lower = ty.to_ascii_lowercase();
1164    let short = lower.rsplit('.').next().unwrap_or(lower.as_str());
1165    let oid = match short {
1166        "bool" | "boolean" => PgOid::Bool,
1167        "int2" | "smallint" => PgOid::Int2,
1168        "int" | "int4" | "integer" => PgOid::Int4,
1169        "int8" | "bigint" => PgOid::Int8,
1170        "float4" | "real" => PgOid::Float4,
1171        "float8" | "double" | "doubleprecision" => PgOid::Float8,
1172        "numeric" | "decimal" => PgOid::Numeric,
1173        "bytea" => PgOid::Bytea,
1174        "json" => PgOid::Json,
1175        "jsonb" => PgOid::Jsonb,
1176        "text" => PgOid::Text,
1177        "varchar" | "character varying" => PgOid::Varchar,
1178        "uuid" => PgOid::Uuid,
1179        "timestamp" => PgOid::Timestamp,
1180        "timestamptz" | "timestampz" => PgOid::TimestampTz,
1181        "vector" => PgOid::Vector,
1182        _ => return None,
1183    };
1184    Some(oid.as_u32())
1185}
1186
1187fn is_row_returning_query(sql: &str) -> bool {
1188    let trimmed = sql.trim_start().to_ascii_lowercase();
1189    trimmed.starts_with("select")
1190        || trimmed.starts_with("with")
1191        || trimmed.starts_with("ask")
1192        || trimmed.starts_with("search")
1193        || trimmed.starts_with("vector")
1194        || trimmed.starts_with("hybrid")
1195}
1196
1197fn is_ask_query(sql: &str) -> bool {
1198    sql.trim_start().to_ascii_lowercase().starts_with("ask")
1199}
1200
1201/// Drive the PG-Wire authentication handshake after a `StartupMessage`.
1202///
1203/// Posture:
1204///   * Auth store enabled → request `AuthenticationCleartextPassword`,
1205///     read the client's `PasswordMessage`, and verify it against the same
1206///     auth store the native transports use. Success installs the resolved
1207///     identity; failure emits `28P01` and closes the socket.
1208///   * No auth store enabled + loopback bind → legacy trust: send
1209///     `AuthenticationOk` with no credentials required (`None` context).
1210///   * No auth store enabled + non-loopback bind → refuse with `28P01`;
1211///     an unconfigured listener must never grant unauthenticated access
1212///     off loopback.
1213///
1214/// TLS posture: a cleartext password may only cross an *unencrypted* link
1215/// on a loopback bind. On a non-loopback bind we refuse to even challenge
1216/// for a password unless the connection is already over TLS (`over_tls`),
1217/// so a client using `sslmode=disable` against a remote listener never
1218/// leaks its password in the clear. Loopback and TLS connections are
1219/// challenged as before.
1220///
1221/// Returns `Ok(Some(context))` once the connection is ready for queries
1222/// (`context` is `None` for trust connections), or `Ok(None)` when
1223/// authentication failed and the error frame has already been sent.
1224///
1225/// Failures give the same `28P01` "password authentication failed" for a
1226/// wrong password and a missing user, so there is no user-existence oracle.
1227async fn authenticate_startup<S>(
1228    stream: &mut S,
1229    runtime: &RedDBRuntime,
1230    config: &PgWireConfig,
1231    over_tls: bool,
1232    params: &super::protocol::StartupParams,
1233) -> Result<Option<Option<PgAuthContext>>, PgWireError>
1234where
1235    S: AsyncRead + AsyncWrite + Unpin,
1236{
1237    let auth_store = runtime.auth_store().filter(|store| store.is_enabled());
1238    let Some(auth_store) = auth_store else {
1239        // No credentials configured: trust survives for loopback only.
1240        if is_loopback_bind(&config.bind_addr) {
1241            send_startup_ready(stream, config, params).await?;
1242            return Ok(Some(None));
1243        }
1244        send_error(stream, "28P01", "password authentication failed").await?;
1245        return Ok(None);
1246    };
1247
1248    // Refuse to challenge for a cleartext password over an unencrypted
1249    // non-loopback link: it would traverse the network in the clear.
1250    // Clients must connect with `sslmode=require` (TLS) to authenticate
1251    // against a remote bind.
1252    if !over_tls && !is_loopback_bind(&config.bind_addr) {
1253        send_error(
1254            stream,
1255            "28000",
1256            "cleartext password authentication requires TLS on non-loopback binds",
1257        )
1258        .await?;
1259        return Ok(None);
1260    }
1261
1262    write_frame(stream, &BackendMessage::AuthenticationCleartextPassword).await?;
1263    let password = match read_frame(stream).await? {
1264        FrontendMessage::PasswordMessage(payload) => password_from_message(&payload),
1265        // Anything other than a PasswordMessage is a protocol violation
1266        // here; treat it as an auth failure rather than leaking detail.
1267        _ => None,
1268    };
1269    let username = params.get("user").unwrap_or("");
1270    let tenant = params.get("tenant");
1271    let Some(password) = password else {
1272        send_error(stream, "28P01", "password authentication failed").await?;
1273        return Ok(None);
1274    };
1275    // A missing user and a wrong password both surface as the same error
1276    // class below — no user-existence oracle leaks over the wire.
1277    let (session, lease) = match auth_store.authenticate_connection(tenant, username, &password) {
1278        Ok(authenticated) => authenticated,
1279        Err(_) => {
1280            send_error(stream, "28P01", "password authentication failed").await?;
1281            return Ok(None);
1282        }
1283    };
1284
1285    send_startup_ready(stream, config, params).await?;
1286    Ok(Some(Some(PgAuthContext {
1287        username: session.username,
1288        role: session.role,
1289        tenant: session.tenant_id,
1290        _lease: lease,
1291    })))
1292}
1293
1294async fn send_startup_ready<S>(
1295    stream: &mut S,
1296    config: &PgWireConfig,
1297    params: &super::protocol::StartupParams,
1298) -> Result<(), PgWireError>
1299where
1300    S: AsyncWrite + Unpin,
1301{
1302    // Authentication has succeeded (or trust applies): signal AuthenticationOk.
1303    write_frame(stream, &BackendMessage::AuthenticationOk).await?;
1304
1305    // Standard ParameterStatus frames. Drivers gate capabilities on these.
1306    for (name, value) in [
1307        ("server_version", config.server_version.as_str()),
1308        ("server_encoding", "UTF8"),
1309        ("client_encoding", "UTF8"),
1310        ("DateStyle", "ISO, MDY"),
1311        ("TimeZone", "UTC"),
1312        ("integer_datetimes", "on"),
1313        ("standard_conforming_strings", "on"),
1314        (
1315            "application_name",
1316            params.get("application_name").unwrap_or(""),
1317        ),
1318    ] {
1319        write_frame(
1320            stream,
1321            &BackendMessage::ParameterStatus {
1322                name: name.to_string(),
1323                value: value.to_string(),
1324            },
1325        )
1326        .await?;
1327    }
1328
1329    // BackendKeyData: (pid, secret_key). CancelRequest is not honoured
1330    // yet, but the key is what a future cancel path authenticates on, so
1331    // it is random per connection rather than a constant a client could
1332    // learn once and reuse against every session.
1333    write_frame(
1334        stream,
1335        &BackendMessage::BackendKeyData {
1336            pid: std::process::id(),
1337            key: random_cancel_key(),
1338        },
1339    )
1340    .await?;
1341
1342    write_frame(
1343        stream,
1344        &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
1345    )
1346    .await?;
1347    Ok(())
1348}
1349
1350async fn handle_simple_query<S>(
1351    stream: &mut S,
1352    runtime: &RedDBRuntime,
1353    auth_context: Option<&PgAuthContext>,
1354    sql: &str,
1355) -> Result<(), PgWireError>
1356where
1357    S: AsyncWrite + Unpin,
1358{
1359    // Empty query convention: PG emits EmptyQueryResponse instead of a
1360    // CommandComplete. Some clients (psql `\;`) rely on this.
1361    if sql.trim().is_empty() {
1362        write_frame(stream, &BackendMessage::EmptyQueryResponse).await?;
1363        write_frame(
1364            stream,
1365            &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
1366        )
1367        .await?;
1368        return Ok(());
1369    }
1370
1371    if let Some(tag) = pg_session_compat_command_tag(sql) {
1372        write_frame(stream, &BackendMessage::CommandComplete(tag.to_string())).await?;
1373        write_frame(
1374            stream,
1375            &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
1376        )
1377        .await?;
1378        return Ok(());
1379    }
1380
1381    let query_result = match translate_pg_catalog_query(runtime, sql) {
1382        Ok(Some(result)) => runtime.own_query_result(crate::runtime::RuntimeQueryResult {
1383            query: sql.to_string(),
1384            mode: reddb_rql::modes::QueryMode::Sql,
1385            statement: "select",
1386            engine: "pg-catalog",
1387            result,
1388            affected_rows: 0,
1389            statement_type: "select",
1390            bookmark: None,
1391            notice: None,
1392        }),
1393        Ok(None) => run_runtime_blocking(|| {
1394            execute_with_pg_auth_context(auth_context, || {
1395                runtime
1396                    .execute_query(sql)
1397                    .and_then(|result| runtime.own_query_result(result))
1398            })
1399        }),
1400        Err(err) => Err(err),
1401    };
1402
1403    match query_result {
1404        Ok(result) => {
1405            emit_success_result(stream, &result).await?;
1406        }
1407        Err(err) => {
1408            // PG SQLSTATE class 42 covers syntax / binding errors; we use
1409            // 42P01 (undefined_table) and 42601 (syntax_error) when we can
1410            // detect; otherwise fall back to XX000 (internal error).
1411            let code = classify_sqlstate(&err.to_string());
1412            send_error(stream, code, &crate::api::client_facing_message(&err)).await?;
1413        }
1414    }
1415
1416    write_frame(
1417        stream,
1418        &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
1419    )
1420    .await?;
1421    Ok(())
1422}
1423
1424fn pg_session_compat_command_tag(sql: &str) -> Option<&'static str> {
1425    let lower = sql.trim().trim_end_matches(';').to_ascii_lowercase();
1426    if lower.starts_with("set ") {
1427        return Some("SET");
1428    }
1429    None
1430}
1431
1432async fn emit_success_result<S>(
1433    stream: &mut S,
1434    result: &crate::runtime::RuntimeQueryResult,
1435) -> Result<(), PgWireError>
1436where
1437    S: AsyncWrite + Unpin,
1438{
1439    if result.statement == "ask" {
1440        emit_ask_result_row(stream, result).await?;
1441        write_frame(
1442            stream,
1443            &BackendMessage::CommandComplete("SELECT 1".to_string()),
1444        )
1445        .await?;
1446    } else if result_returns_rows(result) {
1447        emit_result_rows(stream, &result.result).await?;
1448        write_frame(
1449            stream,
1450            &BackendMessage::CommandComplete(format!("SELECT {}", result.result.records.len())),
1451        )
1452        .await?;
1453    } else {
1454        // DDL / DML / config statements: echo the runtime's
1455        // high-level statement tag back. PG format is
1456        // "<CMD> [<OID>] <COUNT>"; we keep the count where
1457        // applicable and fall back to the runtime's message.
1458        let tag = match result.statement_type {
1459            "insert" => format!("INSERT 0 {}", result.affected_rows),
1460            "update" => format!("UPDATE {}", result.affected_rows),
1461            "delete" => format!("DELETE {}", result.affected_rows),
1462            other => other.to_uppercase(),
1463        };
1464        write_frame(stream, &BackendMessage::CommandComplete(tag)).await?;
1465    }
1466    Ok(())
1467}
1468
1469async fn emit_success_result_without_row_description<S>(
1470    stream: &mut S,
1471    result: &crate::runtime::RuntimeQueryResult,
1472) -> Result<(), PgWireError>
1473where
1474    S: AsyncWrite + Unpin,
1475{
1476    if result.statement == "ask" {
1477        let row = ask_query_result_to_pg_wire_row(result)
1478            .ok_or_else(|| PgWireError::Protocol("ASK result missing row body".to_string()))?;
1479        write_frame(stream, &BackendMessage::DataRow(row.cells)).await?;
1480        write_frame(
1481            stream,
1482            &BackendMessage::CommandComplete("SELECT 1".to_string()),
1483        )
1484        .await?;
1485    } else if result_returns_rows(result) {
1486        emit_result_data_rows(stream, &result.result).await?;
1487        write_frame(
1488            stream,
1489            &BackendMessage::CommandComplete(format!("SELECT {}", result.result.records.len())),
1490        )
1491        .await?;
1492    } else {
1493        let tag = match result.statement_type {
1494            "insert" => format!("INSERT 0 {}", result.affected_rows),
1495            "update" => format!("UPDATE {}", result.affected_rows),
1496            "delete" => format!("DELETE {}", result.affected_rows),
1497            other => other.to_uppercase(),
1498        };
1499        write_frame(stream, &BackendMessage::CommandComplete(tag)).await?;
1500    }
1501    Ok(())
1502}
1503
1504enum PortalExecution {
1505    Complete,
1506    Suspended { next_row_offset: usize },
1507}
1508
1509async fn emit_portal_execution<S>(
1510    stream: &mut S,
1511    result: &crate::runtime::RuntimeQueryResult,
1512    row_description_sent: bool,
1513    row_offset: usize,
1514    max_rows: u32,
1515) -> Result<PortalExecution, String>
1516where
1517    S: AsyncWrite + Unpin,
1518{
1519    if result.statement == "ask" && row_description_sent {
1520        emit_success_result_without_row_description(stream, result)
1521            .await
1522            .map_err(|err| err.to_string())?;
1523        Ok(PortalExecution::Complete)
1524    } else if result.statement == "ask" {
1525        emit_success_result(stream, result)
1526            .await
1527            .map_err(|err| err.to_string())?;
1528        Ok(PortalExecution::Complete)
1529    } else if result_returns_rows(result) {
1530        emit_row_returning_portal_execution(
1531            stream,
1532            result,
1533            row_description_sent,
1534            row_offset,
1535            max_rows,
1536        )
1537        .await
1538    } else if row_description_sent {
1539        emit_success_result_without_row_description(stream, result)
1540            .await
1541            .map_err(|err| err.to_string())?;
1542        Ok(PortalExecution::Complete)
1543    } else {
1544        emit_success_result(stream, result)
1545            .await
1546            .map_err(|err| err.to_string())?;
1547        Ok(PortalExecution::Complete)
1548    }
1549}
1550
1551async fn emit_row_returning_portal_execution<S>(
1552    stream: &mut S,
1553    result: &crate::runtime::RuntimeQueryResult,
1554    row_description_sent: bool,
1555    row_offset: usize,
1556    max_rows: u32,
1557) -> Result<PortalExecution, String>
1558where
1559    S: AsyncWrite + Unpin,
1560{
1561    if !row_description_sent {
1562        emit_result_row_description(stream, &result.result)
1563            .await
1564            .map_err(|err| err.to_string())?;
1565    }
1566
1567    let total_rows = result.result.records.len();
1568    let remaining_rows = total_rows.saturating_sub(row_offset);
1569    let rows_to_emit = if max_rows == 0 {
1570        remaining_rows
1571    } else {
1572        remaining_rows.min(max_rows as usize)
1573    };
1574    emit_result_data_rows_range(stream, &result.result, row_offset, rows_to_emit)
1575        .await
1576        .map_err(|err| err.to_string())?;
1577
1578    let next_row_offset = row_offset + rows_to_emit;
1579    if next_row_offset < total_rows {
1580        write_frame(stream, &BackendMessage::PortalSuspended)
1581            .await
1582            .map_err(|err| err.to_string())?;
1583        Ok(PortalExecution::Suspended { next_row_offset })
1584    } else {
1585        write_frame(
1586            stream,
1587            &BackendMessage::CommandComplete(format!("SELECT {}", total_rows)),
1588        )
1589        .await
1590        .map_err(|err| err.to_string())?;
1591        Ok(PortalExecution::Complete)
1592    }
1593}
1594
1595async fn emit_row_description_for_result<S>(
1596    stream: &mut S,
1597    result: &crate::runtime::RuntimeQueryResult,
1598) -> Result<(), PgWireError>
1599where
1600    S: AsyncWrite + Unpin,
1601{
1602    if result.statement == "ask" {
1603        emit_ask_row_description(stream).await
1604    } else if result_returns_rows(result) {
1605        emit_result_row_description(stream, &result.result).await
1606    } else {
1607        write_frame(stream, &BackendMessage::NoData).await
1608    }
1609}
1610
1611fn result_returns_rows(result: &crate::runtime::RuntimeQueryResult) -> bool {
1612    result.statement_type == "select"
1613}
1614
1615async fn emit_result_rows<S>(
1616    stream: &mut S,
1617    result: &crate::storage::query::unified::UnifiedResult,
1618) -> Result<(), PgWireError>
1619where
1620    S: AsyncWrite + Unpin,
1621{
1622    emit_result_row_description(stream, result).await?;
1623    emit_result_data_rows(stream, result).await
1624}
1625
1626async fn emit_result_row_description<S>(
1627    stream: &mut S,
1628    result: &crate::storage::query::unified::UnifiedResult,
1629) -> Result<(), PgWireError>
1630where
1631    S: AsyncWrite + Unpin,
1632{
1633    // RowDescription: derived from the first record's column ordering.
1634    // When `result.columns` is non-empty we honour that order; otherwise
1635    // we synthesise one from the record's field order.
1636    let columns: Vec<String> = if !result.columns.is_empty() {
1637        result.columns.clone()
1638    } else if let Some(first) = result.records.first() {
1639        record_field_names(first)
1640    } else {
1641        Vec::new()
1642    };
1643
1644    // Peek at the first record for per-column type OIDs. When there's no
1645    // data row we fall back to TEXT for every column — clients render
1646    // empty result sets happily.
1647    let type_oids: Vec<PgOid> = columns
1648        .iter()
1649        .map(|col| {
1650            result
1651                .records
1652                .first()
1653                .and_then(|r| record_get(r, col))
1654                .map(PgOid::from_value)
1655                .unwrap_or(PgOid::Text)
1656        })
1657        .collect();
1658
1659    let descriptors: Vec<ColumnDescriptor> = columns
1660        .iter()
1661        .zip(type_oids.iter())
1662        .map(|(name, oid)| ColumnDescriptor {
1663            name: name.clone(),
1664            table_oid: 0,
1665            column_attr: 0,
1666            type_oid: oid.as_u32(),
1667            type_size: -1,
1668            type_mod: -1,
1669            format: 0,
1670        })
1671        .collect();
1672
1673    write_frame(stream, &BackendMessage::RowDescription(descriptors)).await
1674}
1675
1676async fn emit_result_data_rows<S>(
1677    stream: &mut S,
1678    result: &crate::storage::query::unified::UnifiedResult,
1679) -> Result<(), PgWireError>
1680where
1681    S: AsyncWrite + Unpin,
1682{
1683    emit_result_data_rows_range(stream, result, 0, result.records.len()).await
1684}
1685
1686async fn emit_result_data_rows_range<S>(
1687    stream: &mut S,
1688    result: &crate::storage::query::unified::UnifiedResult,
1689    row_offset: usize,
1690    row_count: usize,
1691) -> Result<(), PgWireError>
1692where
1693    S: AsyncWrite + Unpin,
1694{
1695    let columns: Vec<String> = if !result.columns.is_empty() {
1696        result.columns.clone()
1697    } else if let Some(first) = result.records.first() {
1698        record_field_names(first)
1699    } else {
1700        Vec::new()
1701    };
1702    for record in result.records.iter().skip(row_offset).take(row_count) {
1703        let fields: Vec<Option<Vec<u8>>> = columns
1704            .iter()
1705            .map(|col| record_get(record, col).and_then(value_to_pg_wire_bytes))
1706            .collect();
1707        write_frame(stream, &BackendMessage::DataRow(fields)).await?;
1708    }
1709
1710    Ok(())
1711}
1712
1713async fn emit_ask_result_row<S>(
1714    stream: &mut S,
1715    result: &crate::runtime::RuntimeQueryResult,
1716) -> Result<(), PgWireError>
1717where
1718    S: AsyncWrite + Unpin,
1719{
1720    let row = ask_query_result_to_pg_wire_row(result)
1721        .ok_or_else(|| PgWireError::Protocol("ASK result missing row body".to_string()))?;
1722
1723    emit_ask_row_description(stream).await?;
1724    write_frame(stream, &BackendMessage::DataRow(row.cells)).await?;
1725    Ok(())
1726}
1727
1728async fn emit_ask_row_description<S>(stream: &mut S) -> Result<(), PgWireError>
1729where
1730    S: AsyncWrite + Unpin,
1731{
1732    let descriptors: Vec<ColumnDescriptor> = crate::runtime::ai::pg_wire_ask_row_encoder::columns()
1733        .iter()
1734        .map(|col| ColumnDescriptor {
1735            name: col.name.to_string(),
1736            table_oid: 0,
1737            column_attr: 0,
1738            type_oid: col.oid.as_u32(),
1739            type_size: -1,
1740            type_mod: -1,
1741            format: 0,
1742        })
1743        .collect();
1744    write_frame(stream, &BackendMessage::RowDescription(descriptors)).await
1745}
1746
1747fn ask_query_result_to_pg_wire_row(
1748    result: &crate::runtime::RuntimeQueryResult,
1749) -> Option<crate::runtime::ai::pg_wire_ask_row_encoder::AskRow> {
1750    if result.statement != "ask" {
1751        return None;
1752    }
1753    let ask = crate::presentation::query_result::ask_result_from_unified_result(&result.result)?;
1754
1755    Some(crate::runtime::ai::pg_wire_ask_row_encoder::encode(&ask))
1756}
1757
1758/// Best-effort field lookup on a `UnifiedRecord`. The record API lives in
1759/// `storage::query::unified` and today uses `HashMap<String, Value>` under
1760/// the hood — we use `get` if it exists, else fall back to serialised map.
1761fn record_get<'a>(record: &'a UnifiedRecord, key: &str) -> Option<&'a Value> {
1762    record.get(key)
1763}
1764
1765/// Extract column names in iteration order from a single record. When
1766/// the caller didn't supply an explicit `columns` projection we use the
1767/// first record's field ordering as the canonical tuple shape.
1768///
1769/// HashMap iteration order is non-deterministic — for Phase 3.1 we
1770/// accept the shuffle since PG clients receive the ordered header via
1771/// RowDescription and match cells positionally. A stable ordering
1772/// would require keeping an insertion-order index alongside `values`.
1773fn record_field_names(record: &UnifiedRecord) -> Vec<String> {
1774    // `column_names()` merges the columnar scan side-channel with
1775    // the HashMap so scan rows (which populate only columnar) still
1776    // surface their field names in PG wire output.
1777    record
1778        .column_names()
1779        .into_iter()
1780        .map(|k| k.to_string())
1781        .collect()
1782}
1783
1784async fn send_error<S>(stream: &mut S, code: &str, message: &str) -> Result<(), PgWireError>
1785where
1786    S: AsyncWrite + Unpin,
1787{
1788    write_frame(
1789        stream,
1790        &BackendMessage::ErrorResponse {
1791            severity: "ERROR".to_string(),
1792            code: code.to_string(),
1793            message: message.to_string(),
1794        },
1795    )
1796    .await
1797}
1798
1799/// Heuristically map a runtime error message onto a PG SQLSTATE. Full
1800/// coverage would map every `RedDBError` variant; this is enough for the
1801/// common psql / JDBC paths.
1802fn classify_sqlstate(msg: &str) -> &'static str {
1803    let lower = msg.to_ascii_lowercase();
1804    if lower.contains("not found") || lower.contains("does not exist") {
1805        // 42P01 undefined_table; close enough for collection-not-found.
1806        "42P01"
1807    } else if lower.contains("parse") || lower.contains("expected") || lower.contains("syntax") {
1808        "42601"
1809    } else if lower.contains("already exists") {
1810        "42P07"
1811    } else if lower.contains("permission") || lower.contains("auth") {
1812        "28000"
1813    } else {
1814        "XX000"
1815    }
1816}
1817
1818#[cfg(test)]
1819mod tests {
1820    use super::*;
1821    use crate::api::RedDBOptions;
1822    use crate::auth::{AuthConfig, AuthStore, UserAttributes, UserId};
1823    use crate::runtime::RuntimeQueryResult;
1824    use crate::storage::query::unified::UnifiedResult;
1825    use reddb_rql::modes::QueryMode;
1826    use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
1827
1828    async fn assert_pg_password_rejected(auth: Arc<AuthStore>, username: &str, password: &str) {
1829        let runtime = Arc::new(RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap());
1830        runtime.set_auth_store(auth);
1831        let config = Arc::new(PgWireConfig::default());
1832        let (server_io, mut client_io) = tokio::io::duplex(64 * 1024);
1833        let server = tokio::spawn(async move {
1834            handle_connection(server_io, runtime, config, None)
1835                .await
1836                .unwrap();
1837        });
1838
1839        write_startup_params(&mut client_io, &[("user", username)]).await;
1840        let (tag, body) = read_backend_frame(&mut client_io).await;
1841        assert_eq!(tag, b'R');
1842        assert_eq!(i32::from_be_bytes(body[..4].try_into().unwrap()), 3);
1843        write_frontend_frame(&mut client_io, b'p', cstring_body(password)).await;
1844        let (tag, body) = read_backend_frame(&mut client_io).await;
1845        assert_eq!(tag, b'E');
1846        assert_eq!(error_response_field(&body, b'C'), Some("28P01"));
1847
1848        let mut eof = [0u8; 1];
1849        assert_eq!(client_io.read(&mut eof).await.unwrap(), 0);
1850        server.await.unwrap();
1851    }
1852
1853    #[tokio::test]
1854    async fn response_writer_releases_execution_slot_before_socket_write() {
1855        let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
1856        runtime.configure_execution_admission(1, 0, 100);
1857        let admission = runtime.execution_admission();
1858        let permit = admission.acquire().await.expect("execution permit");
1859        assert_eq!(admission.active(), 1);
1860
1861        let (mut server_io, mut client_io) = tokio::io::duplex(64);
1862        let mut writer = ExecutionResponseWriter::new(&mut server_io, Some(permit));
1863        writer.write_all(b"ok").await.unwrap();
1864        assert_eq!(admission.active(), 0);
1865
1866        let mut received = [0_u8; 2];
1867        client_io.read_exact(&mut received).await.unwrap();
1868        assert_eq!(&received, b"ok");
1869    }
1870
1871    #[tokio::test]
1872    async fn enabled_auth_store_rejects_bad_pgwire_password_with_28p01() {
1873        let runtime = Arc::new(RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap());
1874        let auth = Arc::new(AuthStore::new(AuthConfig {
1875            enabled: true,
1876            require_auth: true,
1877            ..AuthConfig::default()
1878        }));
1879        auth.create_user("alice", "secret", Role::Write).unwrap();
1880        runtime.set_auth_store(auth);
1881
1882        let config = Arc::new(PgWireConfig::default());
1883        let (server_io, mut client_io) = tokio::io::duplex(64 * 1024);
1884        let server = tokio::spawn(async move {
1885            handle_connection(server_io, runtime, config, None)
1886                .await
1887                .unwrap();
1888        });
1889
1890        write_startup_params(&mut client_io, &[("user", "alice")]).await;
1891        // Server must challenge for a cleartext password first.
1892        let (tag, body) = read_backend_frame(&mut client_io).await;
1893        assert_eq!(tag, b'R');
1894        assert_eq!(i32::from_be_bytes(body[..4].try_into().unwrap()), 3);
1895
1896        write_frontend_frame(&mut client_io, b'p', cstring_body("wrong")).await;
1897        let (tag, body) = read_backend_frame(&mut client_io).await;
1898        assert_eq!(tag, b'E');
1899        assert_eq!(error_response_field(&body, b'C'), Some("28P01"));
1900
1901        // The connection is closed after an auth failure.
1902        let mut eof = [0u8; 1];
1903        assert_eq!(client_io.read(&mut eof).await.unwrap(), 0);
1904        server.await.unwrap();
1905    }
1906
1907    #[tokio::test]
1908    async fn pgwire_rejects_expired_accounts() {
1909        let auth = Arc::new(AuthStore::new(AuthConfig {
1910            enabled: true,
1911            require_auth: true,
1912            ..AuthConfig::default()
1913        }));
1914        auth.create_user("alice", "secret", Role::Write).unwrap();
1915        auth.set_user_attributes(
1916            &UserId::platform("alice"),
1917            UserAttributes {
1918                valid_until: Some(1),
1919                ..UserAttributes::default()
1920            },
1921        )
1922        .unwrap();
1923
1924        assert_pg_password_rejected(auth, "alice", "secret").await;
1925    }
1926
1927    #[tokio::test]
1928    async fn pgwire_rejects_accounts_at_connection_limit() {
1929        let auth = Arc::new(AuthStore::new(AuthConfig {
1930            enabled: true,
1931            require_auth: true,
1932            ..AuthConfig::default()
1933        }));
1934        auth.create_user("alice", "secret", Role::Write).unwrap();
1935        let id = UserId::platform("alice");
1936        auth.set_user_attributes(
1937            &id,
1938            UserAttributes {
1939                connection_limit: Some(1),
1940                ..UserAttributes::default()
1941            },
1942        )
1943        .unwrap();
1944        let _held = auth
1945            .acquire_connection_lease(&id)
1946            .expect("occupy the only connection slot");
1947
1948        assert_pg_password_rejected(auth, "alice", "secret").await;
1949    }
1950
1951    #[tokio::test]
1952    async fn pgwire_password_auth_installs_statement_identity() {
1953        let runtime = Arc::new(RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap());
1954        let auth = Arc::new(AuthStore::new(AuthConfig {
1955            enabled: true,
1956            require_auth: true,
1957            ..AuthConfig::default()
1958        }));
1959        auth.create_user_in_tenant(Some("acme"), "alice", "secret", Role::Write)
1960            .unwrap();
1961        runtime.set_auth_store(auth);
1962
1963        let config = Arc::new(PgWireConfig::default());
1964        let (server_io, mut client_io) = tokio::io::duplex(64 * 1024);
1965        let server = tokio::spawn(async move {
1966            handle_connection(server_io, runtime, config, None)
1967                .await
1968                .unwrap();
1969        });
1970
1971        write_startup_params(&mut client_io, &[("user", "alice"), ("tenant", "acme")]).await;
1972        let (tag, body) = read_backend_frame(&mut client_io).await;
1973        assert_eq!(tag, b'R');
1974        assert_eq!(i32::from_be_bytes(body[..4].try_into().unwrap()), 3);
1975        write_frontend_frame(&mut client_io, b'p', cstring_body("secret")).await;
1976        read_until_ready(&mut client_io).await;
1977
1978        // The authenticated identity must drive CURRENT_USER/ROLE/TENANT so
1979        // the simple-query path yields real rows (T,D,C,Z), transport-
1980        // invariant with the native wire.
1981        write_frontend_frame(
1982            &mut client_io,
1983            b'Q',
1984            cstring_body("SELECT CURRENT_USER(), CURRENT_ROLE(), CURRENT_TENANT()"),
1985        )
1986        .await;
1987        let frames = read_until_ready(&mut client_io).await;
1988        assert_eq!(
1989            frames.iter().map(|(tag, _)| *tag).collect::<Vec<_>>(),
1990            b"TDCZ"
1991        );
1992        let cells = decode_data_row(&frames[1].1);
1993        assert_eq!(cells[0].as_deref(), Some(b"alice".as_slice()));
1994        assert_eq!(cells[1].as_deref(), Some(b"write".as_slice()));
1995        assert_eq!(cells[2].as_deref(), Some(b"acme".as_slice()));
1996
1997        write_frontend_frame(&mut client_io, b'X', Vec::new()).await;
1998        server.await.unwrap();
1999    }
2000
2001    #[tokio::test]
2002    async fn extended_parse_bind_execute_returns_rows() {
2003        let runtime = Arc::new(RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap());
2004        let config = Arc::new(PgWireConfig::default());
2005        let (server_io, mut client_io) = tokio::io::duplex(64 * 1024);
2006        let server = tokio::spawn(async move {
2007            handle_connection(server_io, runtime, config, None)
2008                .await
2009                .unwrap();
2010        });
2011
2012        write_startup(&mut client_io).await;
2013        read_until_ready(&mut client_io).await;
2014
2015        write_frontend_frame(
2016            &mut client_io,
2017            b'P',
2018            parse_body("", "SELECT $1::int", &[PgOid::Int4.as_u32()]),
2019        )
2020        .await;
2021        write_frontend_frame(
2022            &mut client_io,
2023            b'B',
2024            bind_body("", "", &[0], &[Some(b"42".as_slice())], &[]),
2025        )
2026        .await;
2027        write_frontend_frame(&mut client_io, b'D', describe_body(b'P', "")).await;
2028        write_frontend_frame(&mut client_io, b'E', execute_body("", 0)).await;
2029        write_frontend_frame(&mut client_io, b'S', Vec::new()).await;
2030
2031        let frames = read_until_ready(&mut client_io).await;
2032        assert_eq!(
2033            frames.iter().map(|(tag, _)| *tag).collect::<Vec<_>>(),
2034            b"12TDCZ"
2035        );
2036        let columns = decode_row_description(&frames[2].1);
2037        assert_eq!(columns.len(), 1);
2038        let cells = decode_data_row(&frames[3].1);
2039        assert_eq!(cells.len(), 1);
2040        assert_eq!(cells[0].as_deref(), Some(b"42".as_slice()));
2041        assert_eq!(decode_command_complete(&frames[4].1), "SELECT 1");
2042
2043        write_frontend_frame(&mut client_io, b'X', Vec::new()).await;
2044        server.await.unwrap();
2045    }
2046
2047    #[test]
2048    fn prepared_pg_utility_statements_preserve_ddl_and_transactions() {
2049        let runtime = RedDBRuntime::in_memory().expect("runtime");
2050        let registry = PreparedRegistry::new();
2051        let execute = |sql: &str, params: Vec<ParamValue>| {
2052            let prepared = registry
2053                .prepare(&runtime, sql)
2054                .expect("prepare utility or DML");
2055            execute_pg_query_result(&runtime, None, &registry, prepared.id, sql, &params)
2056                .unwrap_or_else(|error| panic!("{sql}: {error}"))
2057        };
2058        execute("CREATE TABLE pg_utilities (id INT)", Vec::new());
2059        execute("BEGIN", Vec::new());
2060        execute(
2061            "INSERT INTO pg_utilities (id) VALUES ($1)",
2062            vec![ParamValue::Int64(1)],
2063        );
2064        execute("SAVEPOINT before_second", Vec::new());
2065        execute(
2066            "INSERT INTO pg_utilities (id) VALUES ($1)",
2067            vec![ParamValue::Int64(2)],
2068        );
2069        execute("ROLLBACK TO SAVEPOINT before_second", Vec::new());
2070        execute("COMMIT", Vec::new());
2071        let result = execute("SELECT id FROM pg_utilities", Vec::new());
2072        assert_eq!(result.result.records.len(), 1);
2073        assert_eq!(result.result.records[0].get("id"), Some(&Value::Integer(1)));
2074        execute("DROP TABLE pg_utilities", Vec::new());
2075        assert!(runtime
2076            .db()
2077            .store()
2078            .get_collection("pg_utilities")
2079            .is_none());
2080    }
2081
2082    #[tokio::test]
2083    async fn extended_repeated_execute_parses_prepared_shape_once() {
2084        let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2085        runtime
2086            .execute_query("CREATE TABLE pg_parse_once (id INT)")
2087            .unwrap();
2088        runtime
2089            .execute_query("INSERT INTO pg_parse_once (id) VALUES (42)")
2090            .unwrap();
2091        let prepared_registry = PreparedRegistry::new();
2092        let mut prepared = HashMap::new();
2093        let mut portals = HashMap::new();
2094        let (mut server_io, _client_io) = tokio::io::duplex(64 * 1024);
2095
2096        handle_parse(
2097            &mut server_io,
2098            &runtime,
2099            &prepared_registry,
2100            &mut prepared,
2101            &portals,
2102            super::super::protocol::ParseMessage {
2103                statement: "parse_once_stmt".to_string(),
2104                query: "SELECT id FROM pg_parse_once WHERE id = $1".to_string(),
2105                param_type_oids: vec![PgOid::Int4.as_u32()],
2106            },
2107        )
2108        .await
2109        .unwrap();
2110        assert_eq!(prepared_registry.parse_count(), 1);
2111
2112        handle_bind(
2113            &mut server_io,
2114            &prepared_registry,
2115            &prepared,
2116            &mut portals,
2117            super::super::protocol::BindMessage {
2118                portal: "parse_once_portal".to_string(),
2119                statement: "parse_once_stmt".to_string(),
2120                param_format_codes: vec![0],
2121                params: vec![Some(b"42".to_vec())],
2122                result_format_codes: Vec::new(),
2123            },
2124        )
2125        .await
2126        .unwrap();
2127
2128        for _ in 0..2 {
2129            handle_execute(
2130                &mut server_io,
2131                &runtime,
2132                None,
2133                &prepared_registry,
2134                &mut portals,
2135                super::super::protocol::ExecuteMessage {
2136                    portal: "parse_once_portal".to_string(),
2137                    max_rows: 0,
2138                },
2139            )
2140            .await
2141            .unwrap();
2142        }
2143        assert_eq!(prepared_registry.parse_count(), 1);
2144    }
2145
2146    #[tokio::test]
2147    async fn non_loopback_plaintext_refuses_cleartext_password_without_tls() {
2148        // Posture (issue #1653): a cleartext password may not be challenged
2149        // over an unencrypted non-loopback link. The server must refuse
2150        // *before* sending AuthenticationCleartextPassword, so a driver
2151        // using sslmode=disable against a remote bind never leaks its
2152        // password in the clear. `tls_acceptor = None` + no SSLRequest is
2153        // exactly that plaintext, no-TLS path.
2154        let runtime = Arc::new(RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap());
2155        let auth = Arc::new(AuthStore::new(AuthConfig {
2156            enabled: true,
2157            require_auth: true,
2158            ..AuthConfig::default()
2159        }));
2160        auth.create_user("alice", "secret", Role::Write).unwrap();
2161        runtime.set_auth_store(auth);
2162
2163        // Non-loopback bind: the wildcard address is not a loopback host.
2164        let config = Arc::new(PgWireConfig {
2165            bind_addr: "0.0.0.0:5432".to_string(),
2166            ..PgWireConfig::default()
2167        });
2168        let (server_io, mut client_io) = tokio::io::duplex(64 * 1024);
2169        let server = tokio::spawn(async move {
2170            handle_connection(server_io, runtime, config, None)
2171                .await
2172                .unwrap();
2173        });
2174
2175        write_startup_params(&mut client_io, &[("user", "alice")]).await;
2176        // The refusal arrives directly — never an 'R' cleartext challenge.
2177        let (tag, body) = read_backend_frame(&mut client_io).await;
2178        assert_eq!(tag, b'E', "expected an ErrorResponse, not a challenge");
2179        assert_eq!(error_response_field(&body, b'C'), Some("28000"));
2180
2181        // The connection is closed after the refusal.
2182        let mut eof = [0u8; 1];
2183        assert_eq!(client_io.read(&mut eof).await.unwrap(), 0);
2184        server.await.unwrap();
2185    }
2186
2187    #[test]
2188    fn infer_pg_cast_param_type_oids_from_parameter_casts() {
2189        assert_eq!(
2190            infer_pg_cast_param_type_oids("INSERT INTO t (id, name) VALUES ($1::int, $2::text)"),
2191            vec![(0, PgOid::Int4.as_u32()), (1, PgOid::Text.as_u32())]
2192        );
2193        assert_eq!(
2194            infer_pg_cast_param_type_oids("SEARCH SIMILAR [1.0] COLLECTION v LIMIT $1::int8"),
2195            vec![(0, PgOid::Int8.as_u32())]
2196        );
2197    }
2198
2199    #[test]
2200    fn pg_session_compat_accepts_driver_setup_set_commands() {
2201        assert_eq!(
2202            pg_session_compat_command_tag("SET extra_float_digits = 3"),
2203            Some("SET")
2204        );
2205        assert_eq!(
2206            pg_session_compat_command_tag("SET application_name = 'pgjdbc'"),
2207            Some("SET")
2208        );
2209        assert_eq!(pg_session_compat_command_tag("SELECT 1"), None);
2210    }
2211
2212    #[tokio::test]
2213    async fn ask_success_result_uses_canonical_pg_wire_row_shape() {
2214        let mut result = UnifiedResult::with_columns(vec![
2215            "answer".into(),
2216            "provider".into(),
2217            "model".into(),
2218            "prompt_tokens".into(),
2219            "completion_tokens".into(),
2220            "sources_count".into(),
2221            "sources_flat".into(),
2222            "citations".into(),
2223            "validation".into(),
2224        ]);
2225        let mut record = UnifiedRecord::new();
2226        record.set("answer", Value::text("Deploy failed [^1]."));
2227        record.set("provider", Value::text("openai"));
2228        record.set("model", Value::text("gpt-4o-mini"));
2229        record.set("prompt_tokens", Value::Integer(11));
2230        record.set("completion_tokens", Value::Integer(7));
2231        record.set(
2232            "sources_flat",
2233            Value::Json(
2234                br#"[{"urn":"urn:reddb:row:deployments:1","kind":"row","collection":"deployments","id":"1"}]"#
2235                    .to_vec(),
2236            ),
2237        );
2238        record.set(
2239            "citations",
2240            Value::Json(br#"[{"marker":1,"urn":"urn:reddb:row:deployments:1"}]"#.to_vec()),
2241        );
2242        record.set(
2243            "validation",
2244            Value::Json(br#"{"ok":true,"warnings":[],"errors":[]}"#.to_vec()),
2245        );
2246        result.push(record);
2247
2248        let qr = RuntimeQueryResult {
2249            query: "ASK 'why did deploy fail?'".to_string(),
2250            mode: QueryMode::Sql,
2251            statement: "ask",
2252            engine: "runtime-ai",
2253            result,
2254            affected_rows: 0,
2255            statement_type: "select",
2256            bookmark: None,
2257            notice: None,
2258        };
2259
2260        let mut out = Vec::new();
2261        emit_success_result(&mut out, &qr).await.unwrap();
2262        let frames = decode_frames(&out);
2263
2264        assert_eq!(
2265            frames.iter().map(|(tag, _)| *tag).collect::<Vec<_>>(),
2266            b"TDC"
2267        );
2268
2269        let columns = decode_row_description(frames[0].1);
2270        assert_eq!(
2271            columns,
2272            vec![
2273                ("answer".to_string(), PgOid::Text.as_u32()),
2274                ("cache_hit".to_string(), PgOid::Bool.as_u32()),
2275                ("citations".to_string(), PgOid::Jsonb.as_u32()),
2276                ("completion_tokens".to_string(), PgOid::Int8.as_u32()),
2277                ("cost_usd".to_string(), PgOid::Numeric.as_u32()),
2278                ("mode".to_string(), PgOid::Text.as_u32()),
2279                ("model".to_string(), PgOid::Text.as_u32()),
2280                ("prompt_tokens".to_string(), PgOid::Int8.as_u32()),
2281                ("provider".to_string(), PgOid::Text.as_u32()),
2282                ("retry_count".to_string(), PgOid::Int8.as_u32()),
2283                ("sources_flat".to_string(), PgOid::Jsonb.as_u32()),
2284                ("validation".to_string(), PgOid::Jsonb.as_u32()),
2285            ]
2286        );
2287
2288        let cells = decode_data_row(frames[1].1);
2289        assert_eq!(cells.len(), 12);
2290        assert_eq!(cells[0].as_deref(), Some(b"Deploy failed [^1].".as_slice()));
2291        assert_eq!(cells[1].as_deref(), Some(b"f".as_slice()));
2292        assert_eq!(cells[4].as_deref(), Some(b"0".as_slice()));
2293        assert_eq!(cells[5].as_deref(), Some(b"strict".as_slice()));
2294        assert_eq!(cells[9].as_deref(), Some(b"0".as_slice()));
2295        assert!(std::str::from_utf8(cells[10].as_deref().unwrap())
2296            .unwrap()
2297            .contains(r#""payload""#));
2298        assert_eq!(decode_command_complete(frames[2].1), "SELECT 1");
2299    }
2300
2301    fn decode_frames(bytes: &[u8]) -> Vec<(u8, &[u8])> {
2302        let mut pos = 0;
2303        let mut frames = Vec::new();
2304        while pos < bytes.len() {
2305            let tag = bytes[pos];
2306            let len = u32::from_be_bytes([
2307                bytes[pos + 1],
2308                bytes[pos + 2],
2309                bytes[pos + 3],
2310                bytes[pos + 4],
2311            ]) as usize;
2312            let body_start = pos + 5;
2313            let body_end = pos + 1 + len;
2314            frames.push((tag, &bytes[body_start..body_end]));
2315            pos = body_end;
2316        }
2317        frames
2318    }
2319
2320    fn decode_row_description(body: &[u8]) -> Vec<(String, u32)> {
2321        let count = i16::from_be_bytes([body[0], body[1]]) as usize;
2322        let mut pos = 2;
2323        let mut columns = Vec::with_capacity(count);
2324        for _ in 0..count {
2325            let end = body[pos..].iter().position(|&b| b == 0).unwrap() + pos;
2326            let name = std::str::from_utf8(&body[pos..end]).unwrap().to_string();
2327            pos = end + 1;
2328            pos += 4; // table oid
2329            pos += 2; // column attr
2330            let oid = u32::from_be_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]);
2331            pos += 4;
2332            pos += 2; // type size
2333            pos += 4; // type mod
2334            pos += 2; // format
2335            columns.push((name, oid));
2336        }
2337        columns
2338    }
2339
2340    fn decode_data_row(body: &[u8]) -> Vec<Option<Vec<u8>>> {
2341        let count = i16::from_be_bytes([body[0], body[1]]) as usize;
2342        let mut pos = 2;
2343        let mut cells = Vec::with_capacity(count);
2344        for _ in 0..count {
2345            let len = i32::from_be_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]);
2346            pos += 4;
2347            if len < 0 {
2348                cells.push(None);
2349            } else {
2350                let len = len as usize;
2351                cells.push(Some(body[pos..pos + len].to_vec()));
2352                pos += len;
2353            }
2354        }
2355        cells
2356    }
2357
2358    fn decode_command_complete(body: &[u8]) -> &str {
2359        let nul = body.iter().position(|&b| b == 0).unwrap_or(body.len());
2360        std::str::from_utf8(&body[..nul]).unwrap()
2361    }
2362
2363    async fn write_startup<W: AsyncWrite + Unpin>(stream: &mut W) {
2364        write_startup_params(stream, &[("user", "reddb")]).await;
2365    }
2366
2367    async fn write_startup_params<W: AsyncWrite + Unpin>(stream: &mut W, params: &[(&str, &str)]) {
2368        let mut payload = Vec::new();
2369        payload.extend_from_slice(&crate::wire::postgres::protocol::PG_PROTOCOL_V3.to_be_bytes());
2370        for (name, value) in params {
2371            push_pg_cstring(&mut payload, name);
2372            push_pg_cstring(&mut payload, value);
2373        }
2374        payload.push(0);
2375        let len = (payload.len() + 4) as u32;
2376        stream.write_all(&len.to_be_bytes()).await.unwrap();
2377        stream.write_all(&payload).await.unwrap();
2378    }
2379
2380    fn cstring_body(value: &str) -> Vec<u8> {
2381        let mut out = Vec::new();
2382        push_pg_cstring(&mut out, value);
2383        out
2384    }
2385
2386    fn error_response_field(body: &[u8], field_tag: u8) -> Option<&str> {
2387        let mut pos = 0;
2388        while pos < body.len() && body[pos] != 0 {
2389            let tag = body[pos];
2390            pos += 1;
2391            let end = body[pos..].iter().position(|&b| b == 0)? + pos;
2392            if tag == field_tag {
2393                return std::str::from_utf8(&body[pos..end]).ok();
2394            }
2395            pos = end + 1;
2396        }
2397        None
2398    }
2399
2400    async fn write_frontend_frame<W: AsyncWrite + Unpin>(
2401        stream: &mut W,
2402        tag: u8,
2403        payload: Vec<u8>,
2404    ) {
2405        stream.write_all(&[tag]).await.unwrap();
2406        stream
2407            .write_all(&((payload.len() + 4) as u32).to_be_bytes())
2408            .await
2409            .unwrap();
2410        stream.write_all(&payload).await.unwrap();
2411    }
2412
2413    async fn read_backend_frame<R: AsyncRead + Unpin>(stream: &mut R) -> (u8, Vec<u8>) {
2414        let mut tag = [0u8; 1];
2415        stream.read_exact(&mut tag).await.unwrap();
2416        let mut len = [0u8; 4];
2417        stream.read_exact(&mut len).await.unwrap();
2418        let len = u32::from_be_bytes(len) as usize;
2419        let mut body = vec![0u8; len - 4];
2420        stream.read_exact(&mut body).await.unwrap();
2421        (tag[0], body)
2422    }
2423
2424    async fn read_until_ready<R: AsyncRead + Unpin>(stream: &mut R) -> Vec<(u8, Vec<u8>)> {
2425        let mut frames = Vec::new();
2426        loop {
2427            let frame = read_backend_frame(stream).await;
2428            let done = frame.0 == b'Z';
2429            frames.push(frame);
2430            if done {
2431                return frames;
2432            }
2433        }
2434    }
2435
2436    fn parse_body(statement: &str, query: &str, oids: &[u32]) -> Vec<u8> {
2437        let mut out = Vec::new();
2438        push_pg_cstring(&mut out, statement);
2439        push_pg_cstring(&mut out, query);
2440        out.extend_from_slice(&(oids.len() as i16).to_be_bytes());
2441        for oid in oids {
2442            out.extend_from_slice(&oid.to_be_bytes());
2443        }
2444        out
2445    }
2446
2447    fn bind_body(
2448        portal: &str,
2449        statement: &str,
2450        formats: &[i16],
2451        params: &[Option<&[u8]>],
2452        result_formats: &[i16],
2453    ) -> Vec<u8> {
2454        let mut out = Vec::new();
2455        push_pg_cstring(&mut out, portal);
2456        push_pg_cstring(&mut out, statement);
2457        out.extend_from_slice(&(formats.len() as i16).to_be_bytes());
2458        for format in formats {
2459            out.extend_from_slice(&format.to_be_bytes());
2460        }
2461        out.extend_from_slice(&(params.len() as i16).to_be_bytes());
2462        for param in params {
2463            match param {
2464                Some(bytes) => {
2465                    out.extend_from_slice(&(bytes.len() as i32).to_be_bytes());
2466                    out.extend_from_slice(bytes);
2467                }
2468                None => out.extend_from_slice(&(-1i32).to_be_bytes()),
2469            }
2470        }
2471        out.extend_from_slice(&(result_formats.len() as i16).to_be_bytes());
2472        for format in result_formats {
2473            out.extend_from_slice(&format.to_be_bytes());
2474        }
2475        out
2476    }
2477
2478    fn describe_body(target: u8, name: &str) -> Vec<u8> {
2479        let mut out = vec![target];
2480        push_pg_cstring(&mut out, name);
2481        out
2482    }
2483
2484    fn execute_body(portal: &str, max_rows: u32) -> Vec<u8> {
2485        let mut out = Vec::new();
2486        push_pg_cstring(&mut out, portal);
2487        out.extend_from_slice(&max_rows.to_be_bytes());
2488        out
2489    }
2490
2491    fn push_pg_cstring(out: &mut Vec<u8>, value: &str) {
2492        out.extend_from_slice(value.as_bytes());
2493        out.push(0);
2494    }
2495}
2496
2497/// A fresh 32-bit cancel key from the OS CSPRNG. Falls back to a
2498/// process/time-derived value only if the CSPRNG is unavailable — the
2499/// key guards nothing yet, so refusing the connection would be the
2500/// wrong trade.
2501fn random_cancel_key() -> u32 {
2502    let mut bytes = [0u8; 4];
2503    if crate::crypto::os_random::fill_bytes(&mut bytes).is_ok() {
2504        return u32::from_le_bytes(bytes);
2505    }
2506    let nanos = std::time::SystemTime::now()
2507        .duration_since(std::time::UNIX_EPOCH)
2508        .map(|d| d.subsec_nanos())
2509        .unwrap_or(0);
2510    std::process::id() ^ nanos
2511}