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