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                notice: None,
744            }),
745            Ok(None) => run_runtime_blocking(|| {
746                execute_with_pg_auth_context(auth_context, || runtime.execute_query(sql))
747            })
748            .map_err(|err| err.to_string()),
749            Err(err) => Err(err.to_string()),
750        };
751    }
752
753    run_runtime_blocking(|| {
754        execute_with_pg_auth_context(auth_context, || {
755            runtime.execute_query_with_params(sql, params)
756        })
757    })
758    .map_err(|err| err.to_string())
759}
760
761fn try_execute_pg_scalar_select(
762    sql: &str,
763    params: &[Value],
764) -> Option<crate::runtime::RuntimeQueryResult> {
765    let index = pg_scalar_select_param_index(sql)?;
766    let value = params.get(index)?.clone();
767    let mut result = UnifiedResult::with_columns(vec!["?column?".to_string()]);
768    let mut record = UnifiedRecord::new();
769    record.set("?column?", value);
770    result.push(record);
771    Some(crate::runtime::RuntimeQueryResult {
772        query: sql.to_string(),
773        mode: crate::storage::query::modes::QueryMode::Sql,
774        statement: "select",
775        engine: "pg-wire",
776        result,
777        affected_rows: 0,
778        statement_type: "select",
779        bookmark: None,
780        notice: None,
781    })
782}
783
784fn pg_scalar_select_param_index(sql: &str) -> Option<usize> {
785    let trimmed = sql.trim().trim_end_matches(';').trim();
786    let lower = trimmed.to_ascii_lowercase();
787    let body = lower.strip_prefix("select ")?;
788    let param = if let Some(inner) = body.strip_prefix("cast(") {
789        let end = inner.find(" as ")?;
790        &inner[..end]
791    } else {
792        body.split_whitespace().next()?
793    };
794    let digits = param.strip_prefix('$')?;
795    let n = digits.parse::<usize>().ok()?;
796    n.checked_sub(1)
797}
798
799fn rewrite_pg_parameter_casts(sql: &str) -> String {
800    let mut out = String::with_capacity(sql.len());
801    let bytes = sql.as_bytes();
802    let mut cursor = 0;
803    let mut pos = 0;
804    while pos < bytes.len() {
805        if bytes[pos] != b'$' {
806            pos += 1;
807            continue;
808        }
809        let param_start = pos;
810        pos += 1;
811        let digits_start = pos;
812        while pos < bytes.len() && bytes[pos].is_ascii_digit() {
813            pos += 1;
814        }
815        if digits_start == pos {
816            continue;
817        }
818        if pos + 2 <= bytes.len() && &bytes[pos..pos + 2] == b"::" {
819            let param_end = pos;
820            pos += 2;
821            let type_start = pos;
822            while pos < bytes.len()
823                && (bytes[pos].is_ascii_alphanumeric() || matches!(bytes[pos], b'_' | b'.'))
824            {
825                pos += 1;
826            }
827            if type_start != pos {
828                out.push_str(&sql[cursor..param_start]);
829                out.push_str(&sql[param_start..param_end]);
830                cursor = pos;
831                continue;
832            }
833        }
834    }
835    out.push_str(&sql[cursor..]);
836    out
837}
838
839fn infer_pg_cast_param_type_oids(sql: &str) -> Vec<(usize, u32)> {
840    let mut out = Vec::new();
841    let bytes = sql.as_bytes();
842    let mut pos = 0;
843    while pos < bytes.len() {
844        if bytes[pos] != b'$' {
845            pos += 1;
846            continue;
847        }
848        pos += 1;
849        let digits_start = pos;
850        while pos < bytes.len() && bytes[pos].is_ascii_digit() {
851            pos += 1;
852        }
853        if digits_start == pos {
854            continue;
855        }
856        let Some(param_index) = sql[digits_start..pos]
857            .parse::<usize>()
858            .ok()
859            .and_then(|idx| idx.checked_sub(1))
860        else {
861            continue;
862        };
863        if pos + 2 > bytes.len() || &bytes[pos..pos + 2] != b"::" {
864            continue;
865        }
866        pos += 2;
867        let type_start = pos;
868        while pos < bytes.len()
869            && (bytes[pos].is_ascii_alphanumeric() || matches!(bytes[pos], b'_' | b'.'))
870        {
871            pos += 1;
872        }
873        if type_start == pos {
874            continue;
875        }
876        if let Some(oid) = pg_cast_type_oid(&sql[type_start..pos]) {
877            out.push((param_index, oid));
878        }
879    }
880    out
881}
882
883fn pg_cast_type_oid(ty: &str) -> Option<u32> {
884    let lower = ty.to_ascii_lowercase();
885    let short = lower.rsplit('.').next().unwrap_or(lower.as_str());
886    let oid = match short {
887        "bool" | "boolean" => PgOid::Bool,
888        "int2" | "smallint" => PgOid::Int2,
889        "int" | "int4" | "integer" => PgOid::Int4,
890        "int8" | "bigint" => PgOid::Int8,
891        "float4" | "real" => PgOid::Float4,
892        "float8" | "double" | "doubleprecision" => PgOid::Float8,
893        "numeric" | "decimal" => PgOid::Numeric,
894        "bytea" => PgOid::Bytea,
895        "json" => PgOid::Json,
896        "jsonb" => PgOid::Jsonb,
897        "text" => PgOid::Text,
898        "varchar" | "character varying" => PgOid::Varchar,
899        "uuid" => PgOid::Uuid,
900        "timestamp" => PgOid::Timestamp,
901        "timestamptz" | "timestampz" => PgOid::TimestampTz,
902        "vector" => PgOid::Vector,
903        _ => return None,
904    };
905    Some(oid.as_u32())
906}
907
908fn is_row_returning_query(sql: &str) -> bool {
909    let trimmed = sql.trim_start().to_ascii_lowercase();
910    trimmed.starts_with("select")
911        || trimmed.starts_with("with")
912        || trimmed.starts_with("ask")
913        || trimmed.starts_with("search")
914        || trimmed.starts_with("vector")
915        || trimmed.starts_with("hybrid")
916}
917
918fn is_ask_query(sql: &str) -> bool {
919    sql.trim_start().to_ascii_lowercase().starts_with("ask")
920}
921
922/// Drive the PG-Wire authentication handshake after a `StartupMessage`.
923///
924/// Posture:
925///   * Auth store enabled → request `AuthenticationCleartextPassword`,
926///     read the client's `PasswordMessage`, and verify it against the same
927///     auth store the native transports use. Success installs the resolved
928///     identity; failure emits `28P01` and closes the socket.
929///   * No auth store enabled + loopback bind → legacy trust: send
930///     `AuthenticationOk` with no credentials required (`None` context).
931///   * No auth store enabled + non-loopback bind → refuse with `28P01`;
932///     an unconfigured listener must never grant unauthenticated access
933///     off loopback.
934///
935/// TLS posture: a cleartext password may only cross an *unencrypted* link
936/// on a loopback bind. On a non-loopback bind we refuse to even challenge
937/// for a password unless the connection is already over TLS (`over_tls`),
938/// so a client using `sslmode=disable` against a remote listener never
939/// leaks its password in the clear. Loopback and TLS connections are
940/// challenged as before.
941///
942/// Returns `Ok(Some(context))` once the connection is ready for queries
943/// (`context` is `None` for trust connections), or `Ok(None)` when
944/// authentication failed and the error frame has already been sent.
945///
946/// Failures give the same `28P01` "password authentication failed" for a
947/// wrong password and a missing user, so there is no user-existence oracle.
948async fn authenticate_startup<S>(
949    stream: &mut S,
950    runtime: &RedDBRuntime,
951    config: &PgWireConfig,
952    over_tls: bool,
953    params: &super::protocol::StartupParams,
954) -> Result<Option<Option<PgAuthContext>>, PgWireError>
955where
956    S: AsyncRead + AsyncWrite + Unpin,
957{
958    let auth_store = runtime.auth_store().filter(|store| store.is_enabled());
959    let Some(auth_store) = auth_store else {
960        // No credentials configured: trust survives for loopback only.
961        if is_loopback_bind(&config.bind_addr) {
962            send_startup_ready(stream, config, params).await?;
963            return Ok(Some(None));
964        }
965        send_error(stream, "28P01", "password authentication failed").await?;
966        return Ok(None);
967    };
968
969    // Refuse to challenge for a cleartext password over an unencrypted
970    // non-loopback link: it would traverse the network in the clear.
971    // Clients must connect with `sslmode=require` (TLS) to authenticate
972    // against a remote bind.
973    if !over_tls && !is_loopback_bind(&config.bind_addr) {
974        send_error(
975            stream,
976            "28000",
977            "cleartext password authentication requires TLS on non-loopback binds",
978        )
979        .await?;
980        return Ok(None);
981    }
982
983    write_frame(stream, &BackendMessage::AuthenticationCleartextPassword).await?;
984    let password = match read_frame(stream).await? {
985        FrontendMessage::PasswordMessage(payload) => password_from_message(&payload),
986        // Anything other than a PasswordMessage is a protocol violation
987        // here; treat it as an auth failure rather than leaking detail.
988        _ => None,
989    };
990    let username = params.get("user").unwrap_or("");
991    let tenant = params.get("tenant");
992    let Some(password) = password else {
993        send_error(stream, "28P01", "password authentication failed").await?;
994        return Ok(None);
995    };
996    // A missing user and a wrong password both surface as the same error
997    // class below — no user-existence oracle leaks over the wire.
998    let session = match auth_store.authenticate_in_tenant(tenant, username, &password) {
999        Ok(session) => session,
1000        Err(_) => {
1001            send_error(stream, "28P01", "password authentication failed").await?;
1002            return Ok(None);
1003        }
1004    };
1005
1006    send_startup_ready(stream, config, params).await?;
1007    Ok(Some(Some(PgAuthContext {
1008        username: session.username,
1009        role: session.role,
1010        tenant: session.tenant_id,
1011    })))
1012}
1013
1014async fn send_startup_ready<S>(
1015    stream: &mut S,
1016    config: &PgWireConfig,
1017    params: &super::protocol::StartupParams,
1018) -> Result<(), PgWireError>
1019where
1020    S: AsyncWrite + Unpin,
1021{
1022    // Authentication has succeeded (or trust applies): signal AuthenticationOk.
1023    write_frame(stream, &BackendMessage::AuthenticationOk).await?;
1024
1025    // Standard ParameterStatus frames. Drivers gate capabilities on these.
1026    for (name, value) in [
1027        ("server_version", config.server_version.as_str()),
1028        ("server_encoding", "UTF8"),
1029        ("client_encoding", "UTF8"),
1030        ("DateStyle", "ISO, MDY"),
1031        ("TimeZone", "UTC"),
1032        ("integer_datetimes", "on"),
1033        ("standard_conforming_strings", "on"),
1034        (
1035            "application_name",
1036            params.get("application_name").unwrap_or(""),
1037        ),
1038    ] {
1039        write_frame(
1040            stream,
1041            &BackendMessage::ParameterStatus {
1042                name: name.to_string(),
1043                value: value.to_string(),
1044            },
1045        )
1046        .await?;
1047    }
1048
1049    // BackendKeyData: (pid, secret_key). Used by CancelRequest; we don't
1050    // honour cancels in 3.1 so random-ish values are fine.
1051    write_frame(
1052        stream,
1053        &BackendMessage::BackendKeyData {
1054            pid: std::process::id(),
1055            key: 0xDEADBEEF,
1056        },
1057    )
1058    .await?;
1059
1060    write_frame(
1061        stream,
1062        &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
1063    )
1064    .await?;
1065    Ok(())
1066}
1067
1068async fn handle_simple_query<S>(
1069    stream: &mut S,
1070    runtime: &RedDBRuntime,
1071    auth_context: Option<&PgAuthContext>,
1072    sql: &str,
1073) -> Result<(), PgWireError>
1074where
1075    S: AsyncWrite + Unpin,
1076{
1077    // Empty query convention: PG emits EmptyQueryResponse instead of a
1078    // CommandComplete. Some clients (psql `\;`) rely on this.
1079    if sql.trim().is_empty() {
1080        write_frame(stream, &BackendMessage::EmptyQueryResponse).await?;
1081        write_frame(
1082            stream,
1083            &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
1084        )
1085        .await?;
1086        return Ok(());
1087    }
1088
1089    if let Some(tag) = pg_session_compat_command_tag(sql) {
1090        write_frame(stream, &BackendMessage::CommandComplete(tag.to_string())).await?;
1091        write_frame(
1092            stream,
1093            &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
1094        )
1095        .await?;
1096        return Ok(());
1097    }
1098
1099    let query_result = match translate_pg_catalog_query(runtime, sql) {
1100        Ok(Some(result)) => Ok(crate::runtime::RuntimeQueryResult {
1101            query: sql.to_string(),
1102            mode: crate::storage::query::modes::QueryMode::Sql,
1103            statement: "select",
1104            engine: "pg-catalog",
1105            result,
1106            affected_rows: 0,
1107            statement_type: "select",
1108            bookmark: None,
1109            notice: None,
1110        }),
1111        Ok(None) => run_runtime_blocking(|| {
1112            execute_with_pg_auth_context(auth_context, || runtime.execute_query(sql))
1113        }),
1114        Err(err) => Err(err),
1115    };
1116
1117    match query_result {
1118        Ok(result) => {
1119            emit_success_result(stream, &result).await?;
1120        }
1121        Err(err) => {
1122            // PG SQLSTATE class 42 covers syntax / binding errors; we use
1123            // 42P01 (undefined_table) and 42601 (syntax_error) when we can
1124            // detect; otherwise fall back to XX000 (internal error).
1125            let code = classify_sqlstate(&err.to_string());
1126            send_error(stream, code, &err.to_string()).await?;
1127        }
1128    }
1129
1130    write_frame(
1131        stream,
1132        &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
1133    )
1134    .await?;
1135    Ok(())
1136}
1137
1138fn pg_session_compat_command_tag(sql: &str) -> Option<&'static str> {
1139    let lower = sql.trim().trim_end_matches(';').to_ascii_lowercase();
1140    if lower.starts_with("set ") {
1141        return Some("SET");
1142    }
1143    None
1144}
1145
1146async fn emit_success_result<S>(
1147    stream: &mut S,
1148    result: &crate::runtime::RuntimeQueryResult,
1149) -> Result<(), PgWireError>
1150where
1151    S: AsyncWrite + Unpin,
1152{
1153    if result.statement == "ask" {
1154        emit_ask_result_row(stream, result).await?;
1155        write_frame(
1156            stream,
1157            &BackendMessage::CommandComplete("SELECT 1".to_string()),
1158        )
1159        .await?;
1160    } else if result_returns_rows(result) {
1161        emit_result_rows(stream, &result.result).await?;
1162        write_frame(
1163            stream,
1164            &BackendMessage::CommandComplete(format!("SELECT {}", result.result.records.len())),
1165        )
1166        .await?;
1167    } else {
1168        // DDL / DML / config statements: echo the runtime's
1169        // high-level statement tag back. PG format is
1170        // "<CMD> [<OID>] <COUNT>"; we keep the count where
1171        // applicable and fall back to the runtime's message.
1172        let tag = match result.statement_type {
1173            "insert" => format!("INSERT 0 {}", result.affected_rows),
1174            "update" => format!("UPDATE {}", result.affected_rows),
1175            "delete" => format!("DELETE {}", result.affected_rows),
1176            other => other.to_uppercase(),
1177        };
1178        write_frame(stream, &BackendMessage::CommandComplete(tag)).await?;
1179    }
1180    Ok(())
1181}
1182
1183async fn emit_success_result_without_row_description<S>(
1184    stream: &mut S,
1185    result: &crate::runtime::RuntimeQueryResult,
1186) -> Result<(), PgWireError>
1187where
1188    S: AsyncWrite + Unpin,
1189{
1190    if result.statement == "ask" {
1191        let row = ask_query_result_to_pg_wire_row(result)
1192            .ok_or_else(|| PgWireError::Protocol("ASK result missing row body".to_string()))?;
1193        write_frame(stream, &BackendMessage::DataRow(row.cells)).await?;
1194        write_frame(
1195            stream,
1196            &BackendMessage::CommandComplete("SELECT 1".to_string()),
1197        )
1198        .await?;
1199    } else if result_returns_rows(result) {
1200        emit_result_data_rows(stream, &result.result).await?;
1201        write_frame(
1202            stream,
1203            &BackendMessage::CommandComplete(format!("SELECT {}", result.result.records.len())),
1204        )
1205        .await?;
1206    } else {
1207        let tag = match result.statement_type {
1208            "insert" => format!("INSERT 0 {}", result.affected_rows),
1209            "update" => format!("UPDATE {}", result.affected_rows),
1210            "delete" => format!("DELETE {}", result.affected_rows),
1211            other => other.to_uppercase(),
1212        };
1213        write_frame(stream, &BackendMessage::CommandComplete(tag)).await?;
1214    }
1215    Ok(())
1216}
1217
1218enum PortalExecution {
1219    Complete,
1220    Suspended { next_row_offset: usize },
1221}
1222
1223async fn emit_portal_execution<S>(
1224    stream: &mut S,
1225    result: &crate::runtime::RuntimeQueryResult,
1226    row_description_sent: bool,
1227    row_offset: usize,
1228    max_rows: u32,
1229) -> Result<PortalExecution, String>
1230where
1231    S: AsyncWrite + Unpin,
1232{
1233    if result.statement == "ask" && row_description_sent {
1234        emit_success_result_without_row_description(stream, result)
1235            .await
1236            .map_err(|err| err.to_string())?;
1237        Ok(PortalExecution::Complete)
1238    } else if result.statement == "ask" {
1239        emit_success_result(stream, result)
1240            .await
1241            .map_err(|err| err.to_string())?;
1242        Ok(PortalExecution::Complete)
1243    } else if result_returns_rows(result) {
1244        emit_row_returning_portal_execution(
1245            stream,
1246            result,
1247            row_description_sent,
1248            row_offset,
1249            max_rows,
1250        )
1251        .await
1252    } else if row_description_sent {
1253        emit_success_result_without_row_description(stream, result)
1254            .await
1255            .map_err(|err| err.to_string())?;
1256        Ok(PortalExecution::Complete)
1257    } else {
1258        emit_success_result(stream, result)
1259            .await
1260            .map_err(|err| err.to_string())?;
1261        Ok(PortalExecution::Complete)
1262    }
1263}
1264
1265async fn emit_row_returning_portal_execution<S>(
1266    stream: &mut S,
1267    result: &crate::runtime::RuntimeQueryResult,
1268    row_description_sent: bool,
1269    row_offset: usize,
1270    max_rows: u32,
1271) -> Result<PortalExecution, String>
1272where
1273    S: AsyncWrite + Unpin,
1274{
1275    if !row_description_sent {
1276        emit_result_row_description(stream, &result.result)
1277            .await
1278            .map_err(|err| err.to_string())?;
1279    }
1280
1281    let total_rows = result.result.records.len();
1282    let remaining_rows = total_rows.saturating_sub(row_offset);
1283    let rows_to_emit = if max_rows == 0 {
1284        remaining_rows
1285    } else {
1286        remaining_rows.min(max_rows as usize)
1287    };
1288    emit_result_data_rows_range(stream, &result.result, row_offset, rows_to_emit)
1289        .await
1290        .map_err(|err| err.to_string())?;
1291
1292    let next_row_offset = row_offset + rows_to_emit;
1293    if next_row_offset < total_rows {
1294        write_frame(stream, &BackendMessage::PortalSuspended)
1295            .await
1296            .map_err(|err| err.to_string())?;
1297        Ok(PortalExecution::Suspended { next_row_offset })
1298    } else {
1299        write_frame(
1300            stream,
1301            &BackendMessage::CommandComplete(format!("SELECT {}", total_rows)),
1302        )
1303        .await
1304        .map_err(|err| err.to_string())?;
1305        Ok(PortalExecution::Complete)
1306    }
1307}
1308
1309async fn emit_row_description_for_result<S>(
1310    stream: &mut S,
1311    result: &crate::runtime::RuntimeQueryResult,
1312) -> Result<(), PgWireError>
1313where
1314    S: AsyncWrite + Unpin,
1315{
1316    if result.statement == "ask" {
1317        emit_ask_row_description(stream).await
1318    } else if result_returns_rows(result) {
1319        emit_result_row_description(stream, &result.result).await
1320    } else {
1321        write_frame(stream, &BackendMessage::NoData).await
1322    }
1323}
1324
1325fn result_returns_rows(result: &crate::runtime::RuntimeQueryResult) -> bool {
1326    result.statement_type == "select"
1327}
1328
1329async fn emit_result_rows<S>(
1330    stream: &mut S,
1331    result: &crate::storage::query::unified::UnifiedResult,
1332) -> Result<(), PgWireError>
1333where
1334    S: AsyncWrite + Unpin,
1335{
1336    emit_result_row_description(stream, result).await?;
1337    emit_result_data_rows(stream, result).await
1338}
1339
1340async fn emit_result_row_description<S>(
1341    stream: &mut S,
1342    result: &crate::storage::query::unified::UnifiedResult,
1343) -> Result<(), PgWireError>
1344where
1345    S: AsyncWrite + Unpin,
1346{
1347    // RowDescription: derived from the first record's column ordering.
1348    // When `result.columns` is non-empty we honour that order; otherwise
1349    // we synthesise one from the record's field order.
1350    let columns: Vec<String> = if !result.columns.is_empty() {
1351        result.columns.clone()
1352    } else if let Some(first) = result.records.first() {
1353        record_field_names(first)
1354    } else {
1355        Vec::new()
1356    };
1357
1358    // Peek at the first record for per-column type OIDs. When there's no
1359    // data row we fall back to TEXT for every column — clients render
1360    // empty result sets happily.
1361    let type_oids: Vec<PgOid> = columns
1362        .iter()
1363        .map(|col| {
1364            result
1365                .records
1366                .first()
1367                .and_then(|r| record_get(r, col))
1368                .map(PgOid::from_value)
1369                .unwrap_or(PgOid::Text)
1370        })
1371        .collect();
1372
1373    let descriptors: Vec<ColumnDescriptor> = columns
1374        .iter()
1375        .zip(type_oids.iter())
1376        .map(|(name, oid)| ColumnDescriptor {
1377            name: name.clone(),
1378            table_oid: 0,
1379            column_attr: 0,
1380            type_oid: oid.as_u32(),
1381            type_size: -1,
1382            type_mod: -1,
1383            format: 0,
1384        })
1385        .collect();
1386
1387    write_frame(stream, &BackendMessage::RowDescription(descriptors)).await
1388}
1389
1390async fn emit_result_data_rows<S>(
1391    stream: &mut S,
1392    result: &crate::storage::query::unified::UnifiedResult,
1393) -> Result<(), PgWireError>
1394where
1395    S: AsyncWrite + Unpin,
1396{
1397    emit_result_data_rows_range(stream, result, 0, result.records.len()).await
1398}
1399
1400async fn emit_result_data_rows_range<S>(
1401    stream: &mut S,
1402    result: &crate::storage::query::unified::UnifiedResult,
1403    row_offset: usize,
1404    row_count: usize,
1405) -> Result<(), PgWireError>
1406where
1407    S: AsyncWrite + Unpin,
1408{
1409    let columns: Vec<String> = if !result.columns.is_empty() {
1410        result.columns.clone()
1411    } else if let Some(first) = result.records.first() {
1412        record_field_names(first)
1413    } else {
1414        Vec::new()
1415    };
1416    for record in result.records.iter().skip(row_offset).take(row_count) {
1417        let fields: Vec<Option<Vec<u8>>> = columns
1418            .iter()
1419            .map(|col| record_get(record, col).and_then(value_to_pg_wire_bytes))
1420            .collect();
1421        write_frame(stream, &BackendMessage::DataRow(fields)).await?;
1422    }
1423
1424    Ok(())
1425}
1426
1427async fn emit_ask_result_row<S>(
1428    stream: &mut S,
1429    result: &crate::runtime::RuntimeQueryResult,
1430) -> Result<(), PgWireError>
1431where
1432    S: AsyncWrite + Unpin,
1433{
1434    let row = ask_query_result_to_pg_wire_row(result)
1435        .ok_or_else(|| PgWireError::Protocol("ASK result missing row body".to_string()))?;
1436
1437    emit_ask_row_description(stream).await?;
1438    write_frame(stream, &BackendMessage::DataRow(row.cells)).await?;
1439    Ok(())
1440}
1441
1442async fn emit_ask_row_description<S>(stream: &mut S) -> Result<(), PgWireError>
1443where
1444    S: AsyncWrite + Unpin,
1445{
1446    let descriptors: Vec<ColumnDescriptor> = crate::runtime::ai::pg_wire_ask_row_encoder::columns()
1447        .iter()
1448        .map(|col| ColumnDescriptor {
1449            name: col.name.to_string(),
1450            table_oid: 0,
1451            column_attr: 0,
1452            type_oid: col.oid.as_u32(),
1453            type_size: -1,
1454            type_mod: -1,
1455            format: 0,
1456        })
1457        .collect();
1458    write_frame(stream, &BackendMessage::RowDescription(descriptors)).await
1459}
1460
1461fn ask_query_result_to_pg_wire_row(
1462    result: &crate::runtime::RuntimeQueryResult,
1463) -> Option<crate::runtime::ai::pg_wire_ask_row_encoder::AskRow> {
1464    if result.statement != "ask" {
1465        return None;
1466    }
1467    let record = result.result.records.first()?;
1468    let sources_flat_json =
1469        json_field(record, "sources_flat").unwrap_or(crate::json::Value::Array(Vec::new()));
1470    let citations_json =
1471        json_field(record, "citations").unwrap_or(crate::json::Value::Array(Vec::new()));
1472    let validation_json = json_field(record, "validation")
1473        .unwrap_or_else(|| crate::json::Value::Object(Default::default()));
1474
1475    let effective_mode = match text_field(record, "mode").as_deref() {
1476        Some("lenient") => Mode::Lenient,
1477        _ => Mode::Strict,
1478    };
1479
1480    let ask = AskResult {
1481        answer: text_field(record, "answer")?,
1482        sources_flat: ask_sources_flat(&sources_flat_json),
1483        citations: ask_citations(&citations_json),
1484        validation: ask_validation(&validation_json),
1485        cache_hit: bool_field(record, "cache_hit").unwrap_or(false),
1486        provider: text_field(record, "provider").unwrap_or_default(),
1487        model: text_field(record, "model").unwrap_or_default(),
1488        prompt_tokens: u32_field(record, "prompt_tokens").unwrap_or(0),
1489        completion_tokens: u32_field(record, "completion_tokens").unwrap_or(0),
1490        cost_usd: f64_field(record, "cost_usd").unwrap_or(0.0),
1491        effective_mode,
1492        retry_count: u32_field(record, "retry_count").unwrap_or(0),
1493    };
1494
1495    Some(crate::runtime::ai::pg_wire_ask_row_encoder::encode(&ask))
1496}
1497
1498fn record_field<'a>(record: &'a UnifiedRecord, key: &str) -> Option<&'a Value> {
1499    record.iter_fields().find_map(|(name, value)| {
1500        let name: &str = name;
1501        (name == key).then_some(value)
1502    })
1503}
1504
1505fn text_field(record: &UnifiedRecord, key: &str) -> Option<String> {
1506    match record_field(record, key)? {
1507        Value::Text(s) => Some(s.to_string()),
1508        Value::Email(s) | Value::Url(s) | Value::NodeRef(s) | Value::EdgeRef(s) => Some(s.clone()),
1509        other => Some(other.to_string()),
1510    }
1511}
1512
1513fn bool_field(record: &UnifiedRecord, key: &str) -> Option<bool> {
1514    match record_field(record, key)? {
1515        Value::Boolean(value) => Some(*value),
1516        _ => None,
1517    }
1518}
1519
1520fn u32_field(record: &UnifiedRecord, key: &str) -> Option<u32> {
1521    match record_field(record, key)? {
1522        Value::Integer(n) => (*n >= 0).then_some((*n).min(u32::MAX as i64) as u32),
1523        Value::UnsignedInteger(n) => Some((*n).min(u32::MAX as u64) as u32),
1524        Value::BigInt(n)
1525        | Value::TimestampMs(n)
1526        | Value::Timestamp(n)
1527        | Value::Duration(n)
1528        | Value::Decimal(n) => (*n >= 0).then_some((*n).min(u32::MAX as i64) as u32),
1529        Value::Float(n) => (*n >= 0.0).then_some((*n).min(u32::MAX as f64) as u32),
1530        _ => None,
1531    }
1532}
1533
1534fn f64_field(record: &UnifiedRecord, key: &str) -> Option<f64> {
1535    match record_field(record, key)? {
1536        Value::Integer(n) => Some(*n as f64),
1537        Value::UnsignedInteger(n) => Some(*n as f64),
1538        Value::BigInt(n)
1539        | Value::TimestampMs(n)
1540        | Value::Timestamp(n)
1541        | Value::Duration(n)
1542        | Value::Decimal(n) => Some(*n as f64),
1543        Value::Float(n) => Some(*n),
1544        _ => None,
1545    }
1546}
1547
1548fn json_field(record: &UnifiedRecord, key: &str) -> Option<crate::json::Value> {
1549    match record_field(record, key)? {
1550        // Decode the native binary document-body container (PRD-1398) if present.
1551        Value::Json(bytes) => crate::document_body::decode_container_to_json(bytes)
1552            .or_else(|| crate::json::from_slice(bytes).ok()),
1553        Value::Text(text) => crate::json::from_str(text).ok(),
1554        _ => None,
1555    }
1556}
1557
1558fn ask_sources_flat(value: &crate::json::Value) -> Vec<SourceRow> {
1559    value
1560        .as_array()
1561        .unwrap_or(&[])
1562        .iter()
1563        .filter_map(|source| {
1564            let urn = source
1565                .get("urn")
1566                .and_then(crate::json::Value::as_str)?
1567                .to_string();
1568            let payload = source
1569                .get("payload")
1570                .and_then(crate::json::Value::as_str)
1571                .map(ToString::to_string)
1572                .unwrap_or_else(|| source.to_string_compact());
1573            Some(SourceRow { urn, payload })
1574        })
1575        .collect()
1576}
1577
1578fn ask_citations(value: &crate::json::Value) -> Vec<Citation> {
1579    value
1580        .as_array()
1581        .unwrap_or(&[])
1582        .iter()
1583        .filter_map(|citation| {
1584            let marker = citation
1585                .get("marker")
1586                .and_then(crate::json::Value::as_u64)?;
1587            let urn = citation
1588                .get("urn")
1589                .and_then(crate::json::Value::as_str)?
1590                .to_string();
1591            Some(Citation {
1592                marker: marker.min(u32::MAX as u64) as u32,
1593                urn,
1594            })
1595        })
1596        .collect()
1597}
1598
1599fn ask_validation(value: &crate::json::Value) -> Validation {
1600    Validation {
1601        ok: value
1602            .get("ok")
1603            .and_then(crate::json::Value::as_bool)
1604            .unwrap_or(true),
1605        warnings: validation_items(value, "warnings")
1606            .into_iter()
1607            .map(|(kind, detail)| ValidationWarning { kind, detail })
1608            .collect(),
1609        errors: validation_items(value, "errors")
1610            .into_iter()
1611            .map(|(kind, detail)| ValidationError { kind, detail })
1612            .collect(),
1613    }
1614}
1615
1616fn validation_items(value: &crate::json::Value, key: &str) -> Vec<(String, String)> {
1617    value
1618        .get(key)
1619        .and_then(crate::json::Value::as_array)
1620        .unwrap_or(&[])
1621        .iter()
1622        .filter_map(|item| {
1623            Some((
1624                item.get("kind")
1625                    .and_then(crate::json::Value::as_str)?
1626                    .to_string(),
1627                item.get("detail")
1628                    .and_then(crate::json::Value::as_str)
1629                    .unwrap_or("")
1630                    .to_string(),
1631            ))
1632        })
1633        .collect()
1634}
1635
1636/// Best-effort field lookup on a `UnifiedRecord`. The record API lives in
1637/// `storage::query::unified` and today uses `HashMap<String, Value>` under
1638/// the hood — we use `get` if it exists, else fall back to serialised map.
1639fn record_get<'a>(record: &'a UnifiedRecord, key: &str) -> Option<&'a Value> {
1640    record.get(key)
1641}
1642
1643/// Extract column names in iteration order from a single record. When
1644/// the caller didn't supply an explicit `columns` projection we use the
1645/// first record's field ordering as the canonical tuple shape.
1646///
1647/// HashMap iteration order is non-deterministic — for Phase 3.1 we
1648/// accept the shuffle since PG clients receive the ordered header via
1649/// RowDescription and match cells positionally. A stable ordering
1650/// would require keeping an insertion-order index alongside `values`.
1651fn record_field_names(record: &UnifiedRecord) -> Vec<String> {
1652    // `column_names()` merges the columnar scan side-channel with
1653    // the HashMap so scan rows (which populate only columnar) still
1654    // surface their field names in PG wire output.
1655    record
1656        .column_names()
1657        .into_iter()
1658        .map(|k| k.to_string())
1659        .collect()
1660}
1661
1662async fn send_error<S>(stream: &mut S, code: &str, message: &str) -> Result<(), PgWireError>
1663where
1664    S: AsyncWrite + Unpin,
1665{
1666    write_frame(
1667        stream,
1668        &BackendMessage::ErrorResponse {
1669            severity: "ERROR".to_string(),
1670            code: code.to_string(),
1671            message: message.to_string(),
1672        },
1673    )
1674    .await
1675}
1676
1677/// Heuristically map a runtime error message onto a PG SQLSTATE. Full
1678/// coverage would map every `RedDBError` variant; this is enough for the
1679/// common psql / JDBC paths.
1680fn classify_sqlstate(msg: &str) -> &'static str {
1681    let lower = msg.to_ascii_lowercase();
1682    if lower.contains("not found") || lower.contains("does not exist") {
1683        // 42P01 undefined_table; close enough for collection-not-found.
1684        "42P01"
1685    } else if lower.contains("parse") || lower.contains("expected") || lower.contains("syntax") {
1686        "42601"
1687    } else if lower.contains("already exists") {
1688        "42P07"
1689    } else if lower.contains("permission") || lower.contains("auth") {
1690        "28000"
1691    } else {
1692        "XX000"
1693    }
1694}
1695
1696#[cfg(test)]
1697mod tests {
1698    use super::*;
1699    use crate::api::RedDBOptions;
1700    use crate::auth::{AuthConfig, AuthStore};
1701    use crate::runtime::RuntimeQueryResult;
1702    use crate::storage::query::modes::QueryMode;
1703    use crate::storage::query::unified::UnifiedResult;
1704    use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
1705
1706    #[tokio::test]
1707    async fn enabled_auth_store_rejects_bad_pgwire_password_with_28p01() {
1708        let runtime = Arc::new(RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap());
1709        let auth = Arc::new(AuthStore::new(AuthConfig {
1710            enabled: true,
1711            require_auth: true,
1712            ..AuthConfig::default()
1713        }));
1714        auth.create_user("alice", "secret", Role::Write).unwrap();
1715        runtime.set_auth_store(auth);
1716
1717        let config = Arc::new(PgWireConfig::default());
1718        let (server_io, mut client_io) = tokio::io::duplex(64 * 1024);
1719        let server = tokio::spawn(async move {
1720            handle_connection(server_io, runtime, config, None)
1721                .await
1722                .unwrap();
1723        });
1724
1725        write_startup_params(&mut client_io, &[("user", "alice")]).await;
1726        // Server must challenge for a cleartext password first.
1727        let (tag, body) = read_backend_frame(&mut client_io).await;
1728        assert_eq!(tag, b'R');
1729        assert_eq!(i32::from_be_bytes(body[..4].try_into().unwrap()), 3);
1730
1731        write_frontend_frame(&mut client_io, b'p', cstring_body("wrong")).await;
1732        let (tag, body) = read_backend_frame(&mut client_io).await;
1733        assert_eq!(tag, b'E');
1734        assert_eq!(error_response_field(&body, b'C'), Some("28P01"));
1735
1736        // The connection is closed after an auth failure.
1737        let mut eof = [0u8; 1];
1738        assert_eq!(client_io.read(&mut eof).await.unwrap(), 0);
1739        server.await.unwrap();
1740    }
1741
1742    #[tokio::test]
1743    async fn pgwire_password_auth_installs_statement_identity() {
1744        let runtime = Arc::new(RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap());
1745        let auth = Arc::new(AuthStore::new(AuthConfig {
1746            enabled: true,
1747            require_auth: true,
1748            ..AuthConfig::default()
1749        }));
1750        auth.create_user_in_tenant(Some("acme"), "alice", "secret", Role::Write)
1751            .unwrap();
1752        runtime.set_auth_store(auth);
1753
1754        let config = Arc::new(PgWireConfig::default());
1755        let (server_io, mut client_io) = tokio::io::duplex(64 * 1024);
1756        let server = tokio::spawn(async move {
1757            handle_connection(server_io, runtime, config, None)
1758                .await
1759                .unwrap();
1760        });
1761
1762        write_startup_params(&mut client_io, &[("user", "alice"), ("tenant", "acme")]).await;
1763        let (tag, body) = read_backend_frame(&mut client_io).await;
1764        assert_eq!(tag, b'R');
1765        assert_eq!(i32::from_be_bytes(body[..4].try_into().unwrap()), 3);
1766        write_frontend_frame(&mut client_io, b'p', cstring_body("secret")).await;
1767        read_until_ready(&mut client_io).await;
1768
1769        // The authenticated identity must drive CURRENT_USER/ROLE/TENANT so
1770        // the simple-query path yields real rows (T,D,C,Z), transport-
1771        // invariant with the native wire.
1772        write_frontend_frame(
1773            &mut client_io,
1774            b'Q',
1775            cstring_body("SELECT CURRENT_USER(), CURRENT_ROLE(), CURRENT_TENANT()"),
1776        )
1777        .await;
1778        let frames = read_until_ready(&mut client_io).await;
1779        assert_eq!(
1780            frames.iter().map(|(tag, _)| *tag).collect::<Vec<_>>(),
1781            b"TDCZ"
1782        );
1783        let cells = decode_data_row(&frames[1].1);
1784        assert_eq!(cells[0].as_deref(), Some(b"alice".as_slice()));
1785        assert_eq!(cells[1].as_deref(), Some(b"write".as_slice()));
1786        assert_eq!(cells[2].as_deref(), Some(b"acme".as_slice()));
1787
1788        write_frontend_frame(&mut client_io, b'X', Vec::new()).await;
1789        server.await.unwrap();
1790    }
1791
1792    #[tokio::test]
1793    async fn extended_parse_bind_execute_returns_rows() {
1794        let runtime = Arc::new(RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap());
1795        let config = Arc::new(PgWireConfig::default());
1796        let (server_io, mut client_io) = tokio::io::duplex(64 * 1024);
1797        let server = tokio::spawn(async move {
1798            handle_connection(server_io, runtime, config, None)
1799                .await
1800                .unwrap();
1801        });
1802
1803        write_startup(&mut client_io).await;
1804        read_until_ready(&mut client_io).await;
1805
1806        write_frontend_frame(
1807            &mut client_io,
1808            b'P',
1809            parse_body("", "SELECT $1::int", &[PgOid::Int4.as_u32()]),
1810        )
1811        .await;
1812        write_frontend_frame(
1813            &mut client_io,
1814            b'B',
1815            bind_body("", "", &[0], &[Some(b"42".as_slice())], &[]),
1816        )
1817        .await;
1818        write_frontend_frame(&mut client_io, b'D', describe_body(b'P', "")).await;
1819        write_frontend_frame(&mut client_io, b'E', execute_body("", 0)).await;
1820        write_frontend_frame(&mut client_io, b'S', Vec::new()).await;
1821
1822        let frames = read_until_ready(&mut client_io).await;
1823        assert_eq!(
1824            frames.iter().map(|(tag, _)| *tag).collect::<Vec<_>>(),
1825            b"12TDCZ"
1826        );
1827        let columns = decode_row_description(&frames[2].1);
1828        assert_eq!(columns.len(), 1);
1829        let cells = decode_data_row(&frames[3].1);
1830        assert_eq!(cells.len(), 1);
1831        assert_eq!(cells[0].as_deref(), Some(b"42".as_slice()));
1832        assert_eq!(decode_command_complete(&frames[4].1), "SELECT 1");
1833
1834        write_frontend_frame(&mut client_io, b'X', Vec::new()).await;
1835        server.await.unwrap();
1836    }
1837
1838    #[tokio::test]
1839    async fn non_loopback_plaintext_refuses_cleartext_password_without_tls() {
1840        // Posture (issue #1653): a cleartext password may not be challenged
1841        // over an unencrypted non-loopback link. The server must refuse
1842        // *before* sending AuthenticationCleartextPassword, so a driver
1843        // using sslmode=disable against a remote bind never leaks its
1844        // password in the clear. `tls_acceptor = None` + no SSLRequest is
1845        // exactly that plaintext, no-TLS path.
1846        let runtime = Arc::new(RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap());
1847        let auth = Arc::new(AuthStore::new(AuthConfig {
1848            enabled: true,
1849            require_auth: true,
1850            ..AuthConfig::default()
1851        }));
1852        auth.create_user("alice", "secret", Role::Write).unwrap();
1853        runtime.set_auth_store(auth);
1854
1855        // Non-loopback bind: the wildcard address is not a loopback host.
1856        let config = Arc::new(PgWireConfig {
1857            bind_addr: "0.0.0.0:5432".to_string(),
1858            ..PgWireConfig::default()
1859        });
1860        let (server_io, mut client_io) = tokio::io::duplex(64 * 1024);
1861        let server = tokio::spawn(async move {
1862            handle_connection(server_io, runtime, config, None)
1863                .await
1864                .unwrap();
1865        });
1866
1867        write_startup_params(&mut client_io, &[("user", "alice")]).await;
1868        // The refusal arrives directly — never an 'R' cleartext challenge.
1869        let (tag, body) = read_backend_frame(&mut client_io).await;
1870        assert_eq!(tag, b'E', "expected an ErrorResponse, not a challenge");
1871        assert_eq!(error_response_field(&body, b'C'), Some("28000"));
1872
1873        // The connection is closed after the refusal.
1874        let mut eof = [0u8; 1];
1875        assert_eq!(client_io.read(&mut eof).await.unwrap(), 0);
1876        server.await.unwrap();
1877    }
1878
1879    #[test]
1880    fn infer_pg_cast_param_type_oids_from_parameter_casts() {
1881        assert_eq!(
1882            infer_pg_cast_param_type_oids("INSERT INTO t (id, name) VALUES ($1::int, $2::text)"),
1883            vec![(0, PgOid::Int4.as_u32()), (1, PgOid::Text.as_u32())]
1884        );
1885        assert_eq!(
1886            infer_pg_cast_param_type_oids("SEARCH SIMILAR [1.0] COLLECTION v LIMIT $1::int8"),
1887            vec![(0, PgOid::Int8.as_u32())]
1888        );
1889    }
1890
1891    #[test]
1892    fn pg_session_compat_accepts_driver_setup_set_commands() {
1893        assert_eq!(
1894            pg_session_compat_command_tag("SET extra_float_digits = 3"),
1895            Some("SET")
1896        );
1897        assert_eq!(
1898            pg_session_compat_command_tag("SET application_name = 'pgjdbc'"),
1899            Some("SET")
1900        );
1901        assert_eq!(pg_session_compat_command_tag("SELECT 1"), None);
1902    }
1903
1904    #[tokio::test]
1905    async fn ask_success_result_uses_canonical_pg_wire_row_shape() {
1906        let mut result = UnifiedResult::with_columns(vec![
1907            "answer".into(),
1908            "provider".into(),
1909            "model".into(),
1910            "prompt_tokens".into(),
1911            "completion_tokens".into(),
1912            "sources_count".into(),
1913            "sources_flat".into(),
1914            "citations".into(),
1915            "validation".into(),
1916        ]);
1917        let mut record = UnifiedRecord::new();
1918        record.set("answer", Value::text("Deploy failed [^1]."));
1919        record.set("provider", Value::text("openai"));
1920        record.set("model", Value::text("gpt-4o-mini"));
1921        record.set("prompt_tokens", Value::Integer(11));
1922        record.set("completion_tokens", Value::Integer(7));
1923        record.set(
1924            "sources_flat",
1925            Value::Json(
1926                br#"[{"urn":"urn:reddb:row:deployments:1","kind":"row","collection":"deployments","id":"1"}]"#
1927                    .to_vec(),
1928            ),
1929        );
1930        record.set(
1931            "citations",
1932            Value::Json(br#"[{"marker":1,"urn":"urn:reddb:row:deployments:1"}]"#.to_vec()),
1933        );
1934        record.set(
1935            "validation",
1936            Value::Json(br#"{"ok":true,"warnings":[],"errors":[]}"#.to_vec()),
1937        );
1938        result.push(record);
1939
1940        let qr = RuntimeQueryResult {
1941            query: "ASK 'why did deploy fail?'".to_string(),
1942            mode: QueryMode::Sql,
1943            statement: "ask",
1944            engine: "runtime-ai",
1945            result,
1946            affected_rows: 0,
1947            statement_type: "select",
1948            bookmark: None,
1949            notice: None,
1950        };
1951
1952        let mut out = Vec::new();
1953        emit_success_result(&mut out, &qr).await.unwrap();
1954        let frames = decode_frames(&out);
1955
1956        assert_eq!(
1957            frames.iter().map(|(tag, _)| *tag).collect::<Vec<_>>(),
1958            b"TDC"
1959        );
1960
1961        let columns = decode_row_description(frames[0].1);
1962        assert_eq!(
1963            columns,
1964            vec![
1965                ("answer".to_string(), PgOid::Text.as_u32()),
1966                ("cache_hit".to_string(), PgOid::Bool.as_u32()),
1967                ("citations".to_string(), PgOid::Jsonb.as_u32()),
1968                ("completion_tokens".to_string(), PgOid::Int8.as_u32()),
1969                ("cost_usd".to_string(), PgOid::Numeric.as_u32()),
1970                ("mode".to_string(), PgOid::Text.as_u32()),
1971                ("model".to_string(), PgOid::Text.as_u32()),
1972                ("prompt_tokens".to_string(), PgOid::Int8.as_u32()),
1973                ("provider".to_string(), PgOid::Text.as_u32()),
1974                ("retry_count".to_string(), PgOid::Int8.as_u32()),
1975                ("sources_flat".to_string(), PgOid::Jsonb.as_u32()),
1976                ("validation".to_string(), PgOid::Jsonb.as_u32()),
1977            ]
1978        );
1979
1980        let cells = decode_data_row(frames[1].1);
1981        assert_eq!(cells.len(), 12);
1982        assert_eq!(cells[0].as_deref(), Some(b"Deploy failed [^1].".as_slice()));
1983        assert_eq!(cells[1].as_deref(), Some(b"f".as_slice()));
1984        assert_eq!(cells[4].as_deref(), Some(b"0".as_slice()));
1985        assert_eq!(cells[5].as_deref(), Some(b"strict".as_slice()));
1986        assert_eq!(cells[9].as_deref(), Some(b"0".as_slice()));
1987        assert!(std::str::from_utf8(cells[10].as_deref().unwrap())
1988            .unwrap()
1989            .contains(r#""payload""#));
1990        assert_eq!(decode_command_complete(frames[2].1), "SELECT 1");
1991    }
1992
1993    fn decode_frames(bytes: &[u8]) -> Vec<(u8, &[u8])> {
1994        let mut pos = 0;
1995        let mut frames = Vec::new();
1996        while pos < bytes.len() {
1997            let tag = bytes[pos];
1998            let len = u32::from_be_bytes([
1999                bytes[pos + 1],
2000                bytes[pos + 2],
2001                bytes[pos + 3],
2002                bytes[pos + 4],
2003            ]) as usize;
2004            let body_start = pos + 5;
2005            let body_end = pos + 1 + len;
2006            frames.push((tag, &bytes[body_start..body_end]));
2007            pos = body_end;
2008        }
2009        frames
2010    }
2011
2012    fn decode_row_description(body: &[u8]) -> Vec<(String, u32)> {
2013        let count = i16::from_be_bytes([body[0], body[1]]) as usize;
2014        let mut pos = 2;
2015        let mut columns = Vec::with_capacity(count);
2016        for _ in 0..count {
2017            let end = body[pos..].iter().position(|&b| b == 0).unwrap() + pos;
2018            let name = std::str::from_utf8(&body[pos..end]).unwrap().to_string();
2019            pos = end + 1;
2020            pos += 4; // table oid
2021            pos += 2; // column attr
2022            let oid = u32::from_be_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]);
2023            pos += 4;
2024            pos += 2; // type size
2025            pos += 4; // type mod
2026            pos += 2; // format
2027            columns.push((name, oid));
2028        }
2029        columns
2030    }
2031
2032    fn decode_data_row(body: &[u8]) -> Vec<Option<Vec<u8>>> {
2033        let count = i16::from_be_bytes([body[0], body[1]]) as usize;
2034        let mut pos = 2;
2035        let mut cells = Vec::with_capacity(count);
2036        for _ in 0..count {
2037            let len = i32::from_be_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]);
2038            pos += 4;
2039            if len < 0 {
2040                cells.push(None);
2041            } else {
2042                let len = len as usize;
2043                cells.push(Some(body[pos..pos + len].to_vec()));
2044                pos += len;
2045            }
2046        }
2047        cells
2048    }
2049
2050    fn decode_command_complete(body: &[u8]) -> &str {
2051        let nul = body.iter().position(|&b| b == 0).unwrap_or(body.len());
2052        std::str::from_utf8(&body[..nul]).unwrap()
2053    }
2054
2055    async fn write_startup<W: AsyncWrite + Unpin>(stream: &mut W) {
2056        write_startup_params(stream, &[("user", "reddb")]).await;
2057    }
2058
2059    async fn write_startup_params<W: AsyncWrite + Unpin>(stream: &mut W, params: &[(&str, &str)]) {
2060        let mut payload = Vec::new();
2061        payload.extend_from_slice(&crate::wire::postgres::protocol::PG_PROTOCOL_V3.to_be_bytes());
2062        for (name, value) in params {
2063            push_pg_cstring(&mut payload, name);
2064            push_pg_cstring(&mut payload, value);
2065        }
2066        payload.push(0);
2067        let len = (payload.len() + 4) as u32;
2068        stream.write_all(&len.to_be_bytes()).await.unwrap();
2069        stream.write_all(&payload).await.unwrap();
2070    }
2071
2072    fn cstring_body(value: &str) -> Vec<u8> {
2073        let mut out = Vec::new();
2074        push_pg_cstring(&mut out, value);
2075        out
2076    }
2077
2078    fn error_response_field(body: &[u8], field_tag: u8) -> Option<&str> {
2079        let mut pos = 0;
2080        while pos < body.len() && body[pos] != 0 {
2081            let tag = body[pos];
2082            pos += 1;
2083            let end = body[pos..].iter().position(|&b| b == 0)? + pos;
2084            if tag == field_tag {
2085                return std::str::from_utf8(&body[pos..end]).ok();
2086            }
2087            pos = end + 1;
2088        }
2089        None
2090    }
2091
2092    async fn write_frontend_frame<W: AsyncWrite + Unpin>(
2093        stream: &mut W,
2094        tag: u8,
2095        payload: Vec<u8>,
2096    ) {
2097        stream.write_all(&[tag]).await.unwrap();
2098        stream
2099            .write_all(&((payload.len() + 4) as u32).to_be_bytes())
2100            .await
2101            .unwrap();
2102        stream.write_all(&payload).await.unwrap();
2103    }
2104
2105    async fn read_backend_frame<R: AsyncRead + Unpin>(stream: &mut R) -> (u8, Vec<u8>) {
2106        let mut tag = [0u8; 1];
2107        stream.read_exact(&mut tag).await.unwrap();
2108        let mut len = [0u8; 4];
2109        stream.read_exact(&mut len).await.unwrap();
2110        let len = u32::from_be_bytes(len) as usize;
2111        let mut body = vec![0u8; len - 4];
2112        stream.read_exact(&mut body).await.unwrap();
2113        (tag[0], body)
2114    }
2115
2116    async fn read_until_ready<R: AsyncRead + Unpin>(stream: &mut R) -> Vec<(u8, Vec<u8>)> {
2117        let mut frames = Vec::new();
2118        loop {
2119            let frame = read_backend_frame(stream).await;
2120            let done = frame.0 == b'Z';
2121            frames.push(frame);
2122            if done {
2123                return frames;
2124            }
2125        }
2126    }
2127
2128    fn parse_body(statement: &str, query: &str, oids: &[u32]) -> Vec<u8> {
2129        let mut out = Vec::new();
2130        push_pg_cstring(&mut out, statement);
2131        push_pg_cstring(&mut out, query);
2132        out.extend_from_slice(&(oids.len() as i16).to_be_bytes());
2133        for oid in oids {
2134            out.extend_from_slice(&oid.to_be_bytes());
2135        }
2136        out
2137    }
2138
2139    fn bind_body(
2140        portal: &str,
2141        statement: &str,
2142        formats: &[i16],
2143        params: &[Option<&[u8]>],
2144        result_formats: &[i16],
2145    ) -> Vec<u8> {
2146        let mut out = Vec::new();
2147        push_pg_cstring(&mut out, portal);
2148        push_pg_cstring(&mut out, statement);
2149        out.extend_from_slice(&(formats.len() as i16).to_be_bytes());
2150        for format in formats {
2151            out.extend_from_slice(&format.to_be_bytes());
2152        }
2153        out.extend_from_slice(&(params.len() as i16).to_be_bytes());
2154        for param in params {
2155            match param {
2156                Some(bytes) => {
2157                    out.extend_from_slice(&(bytes.len() as i32).to_be_bytes());
2158                    out.extend_from_slice(bytes);
2159                }
2160                None => out.extend_from_slice(&(-1i32).to_be_bytes()),
2161            }
2162        }
2163        out.extend_from_slice(&(result_formats.len() as i16).to_be_bytes());
2164        for format in result_formats {
2165            out.extend_from_slice(&format.to_be_bytes());
2166        }
2167        out
2168    }
2169
2170    fn describe_body(target: u8, name: &str) -> Vec<u8> {
2171        let mut out = vec![target];
2172        push_pg_cstring(&mut out, name);
2173        out
2174    }
2175
2176    fn execute_body(portal: &str, max_rows: u32) -> Vec<u8> {
2177        let mut out = Vec::new();
2178        push_pg_cstring(&mut out, portal);
2179        out.extend_from_slice(&max_rows.to_be_bytes());
2180        out
2181    }
2182
2183    fn push_pg_cstring(out: &mut Vec<u8>, value: &str) {
2184        out.extend_from_slice(value.as_bytes());
2185        out.push(0);
2186    }
2187}