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