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