1use 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#[derive(Debug, Clone)]
30pub struct PgWireConfig {
31 pub bind_addr: String,
34 pub server_version: String,
38 pub tls: Option<crate::wire::tls::WireTlsConfig>,
47}
48
49#[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
89fn 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
116fn 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
134struct 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
166fn 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
182fn 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
191pub 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 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 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
250pub(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 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 write_raw_byte(&mut stream, b'N').await?;
283 continue;
284 }
285 FrontendMessage::GssEncRequest => {
286 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 return Ok(());
297 }
298 other => {
299 return Err(PgWireError::Protocol(format!(
300 "unexpected startup frame: {other:?}"
301 )));
302 }
303 }
304 }
305}
306
307async 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
338async 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, ¶ms).await? {
353 Some(context) => context,
354 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 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 continue;
409 }
410 FrontendMessage::Sync => {
411 write_frame(
412 &mut stream,
413 &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
414 )
415 .await?;
416 }
417 FrontendMessage::PasswordMessage(_) => {
418 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
922async 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 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 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 _ => 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 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 write_frame(stream, &BackendMessage::AuthenticationOk).await?;
1024
1025 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 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 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 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 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 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 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 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
1636fn record_get<'a>(record: &'a UnifiedRecord, key: &str) -> Option<&'a Value> {
1640 record.get(key)
1641}
1642
1643fn record_field_names(record: &UnifiedRecord) -> Vec<String> {
1652 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
1677fn 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"
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 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 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 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 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 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 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 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; pos += 2; let oid = u32::from_be_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]);
2023 pos += 4;
2024 pos += 2; pos += 4; pos += 2; 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}