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