use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use bytes::Bytes;
use serde_json::Value;
use proxima_pgwire::{PgClientConfig, PgReply, PgwireClientUpstream, QueryRequest};
use proxima_primitives::pipe::SendPipe;
use proxima_primitives::pipe::handler::{PipeHandle, into_handle};
use proxima_primitives::pipe::pipe_factory::PipeFactory;
use proxima_primitives::pipe::request::{Request, Response};
use crate::PrimeTcpUpstream;
use crate::client::handle::ClientProtocol;
use crate::error::ProximaError;
#[derive(Debug, Default)]
pub struct PgwirePipeFactory;
impl PgwirePipeFactory {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl PipeFactory for PgwirePipeFactory {
fn name(&self) -> &str {
"pgwire"
}
fn build(
&self,
spec: &Value,
_inner: Option<PipeHandle>,
) -> Pin<Box<dyn Future<Output = Result<PipeHandle, ProximaError>> + Send + '_>> {
let spec = spec.clone();
Box::pin(async move {
let config = config_from_spec(&spec)?;
let upstream = PrimeTcpUpstream::with_host(config.host.clone(), config.port);
Ok(into_handle(PgwireClientPipe::new(
PgwireClientUpstream::new(upstream, config),
)))
})
}
}
struct PgwireClientPipe {
inner: PgwireClientUpstream<PrimeTcpUpstream>,
}
impl PgwireClientPipe {
fn new(inner: PgwireClientUpstream<PrimeTcpUpstream>) -> Self {
Self { inner }
}
}
impl SendPipe for PgwireClientPipe {
type In = Request<Bytes>;
type Out = Response<Bytes>;
type Err = ProximaError;
fn call(
&self,
request: Request<Bytes>,
) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
async move {
let (request, body) = request.body_bytes().await?;
let sql = String::from_utf8_lossy(&body).into_owned();
let query = QueryRequest::try_from_wire(request.method.as_bytes(), "", sql, Vec::new())
.map_err(|message| {
ProximaError::Upstream(format!("pgwire client: protocol: {message}"))
})?;
let reply = self.inner.call(query).await?;
Ok(pg_reply_to_bytes(&reply))
}
}
}
fn pg_reply_to_bytes(reply: &PgReply) -> Response<Bytes> {
let body = serde_json::to_vec(&pg_reply_to_json(reply))
.unwrap_or_else(|err| format!("{{\"encode_error\":\"{err}\"}}").into_bytes());
Response::new(200).with_payload(body)
}
fn pg_reply_to_json(reply: &PgReply) -> Value {
match reply {
PgReply::Query(query) => serde_json::json!({
"kind": "query",
"columns": query.columns.iter().map(|column| serde_json::json!({
"name": column.name,
"type_oid": column.type_oid.0,
})).collect::<Vec<_>>(),
"rows": query.rows.iter().map(|row| {
row.iter().map(sql_value_to_json).collect::<Vec<_>>()
}).collect::<Vec<_>>(),
"command_tag": query.command_tag,
}),
PgReply::Error(error) => serde_json::json!({
"kind": "error",
"severity": error.severity,
"sqlstate": error.sqlstate,
"message": error.message,
"detail": error.detail,
"hint": error.hint,
}),
other => serde_json::json!({ "kind": "unsupported", "debug": format!("{other:?}") }),
}
}
fn sql_value_to_json(value: &proxima_pgwire::SqlValue) -> Value {
match value {
proxima_pgwire::SqlValue::Null => Value::Null,
proxima_pgwire::SqlValue::Int(number) => serde_json::json!(number),
proxima_pgwire::SqlValue::Float(number) => serde_json::json!(number),
proxima_pgwire::SqlValue::Bool(flag) => serde_json::json!(flag),
proxima_pgwire::SqlValue::Text(text) => serde_json::json!(text),
proxima_pgwire::SqlValue::Bytes(bytes) => serde_json::json!(bytes),
}
}
fn config_from_spec(spec: &Value) -> Result<PgClientConfig, ProximaError> {
if let Some(dsn) = spec.get("dsn").and_then(Value::as_str) {
return PgClientConfig::from_dsn(dsn)
.map_err(|err| ProximaError::Config(format!("pgwire dsn: {err}")));
}
serde_json::from_value(spec.clone())
.map_err(|err| ProximaError::Config(format!("pgwire config: {err}")))
}
pub struct PgwireClientProtocol {
dsn: String,
}
impl PgwireClientProtocol {
#[must_use]
pub fn dsn(dsn: impl Into<String>) -> Self {
Self { dsn: dsn.into() }
}
}
impl ClientProtocol for PgwireClientProtocol {
fn spec(&self) -> Value {
serde_json::json!({"type": "pgwire", "dsn": self.dsn})
}
fn factory(&self) -> Arc<dyn PipeFactory> {
Arc::new(PgwirePipeFactory::new())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::client::protocol::ClientProtocolExt;
#[test]
fn config_from_dsn_spec() {
let spec = serde_json::json!({ "type": "pgwire", "dsn": "postgres://u:p@h:6543/db" });
let config = config_from_spec(&spec).expect("config");
assert_eq!((config.host.as_str(), config.port), ("h", 6543));
assert_eq!(
(config.user.as_str(), config.database.as_str()),
("u", "db")
);
}
#[test]
fn config_from_fields_ignores_type_discriminator() {
let spec =
serde_json::json!({ "type": "pgwire", "host": "db", "port": 5544, "user": "alice" });
let config = config_from_spec(&spec).expect("config");
assert_eq!(
(config.host.as_str(), config.port, config.user.as_str()),
("db", 5544, "alice")
);
assert_eq!(
config.database, "postgres",
"unspecified field falls back to default"
);
}
#[test]
fn factory_name_is_the_spec_key() {
assert_eq!(PgwirePipeFactory::new().name(), "pgwire");
}
#[test]
fn client_protocol_lowers_to_the_type_and_dsn_spec() {
let protocol = PgwireClientProtocol::dsn("postgres://u:p@h:5432/db");
let spec = protocol.spec();
assert_eq!(spec["type"], "pgwire");
assert_eq!(spec["dsn"], "postgres://u:p@h:5432/db");
assert_eq!(protocol.factory().name(), "pgwire");
}
#[test]
fn unsupported_method_is_rejected_before_ever_touching_the_network() {
let config = PgClientConfig::builder()
.host("127.0.0.1")
.port(0)
.user("nobody")
.password(String::new())
.database("nobody")
.build();
let upstream = PrimeTcpUpstream::with_host(config.host.clone(), config.port);
let pipe = PgwireClientPipe::new(PgwireClientUpstream::new(upstream, config));
let request = Request::builder()
.method("COPY_DATA")
.path("/")
.body("drop table users")
.build()
.expect("builder");
let error = futures::executor::block_on(pipe.call(request))
.expect_err("an unsupported method must be rejected, not executed as SQL");
assert!(
format!("{error}").contains("unsupported client verb"),
"got: {error}"
);
}
#[cfg(all(
feature = "runtime-prime-executor",
feature = "runtime-prime-inbox-alloc",
feature = "runtime-prime-reactor",
feature = "runtime-prime-bgpool"
))]
#[test]
fn pgwire_through_client_queries_real_postgres() {
let Ok(host) = std::env::var("PGWIRE_REALPG_HOST") else {
eprintln!("skipping pgwire_through_client: PGWIRE_REALPG_HOST unset (no server)");
return;
};
let port = std::env::var("PGWIRE_REALPG_PORT").unwrap_or_else(|_| "5432".to_string());
let password =
std::env::var("PGWIRE_REALPG_PASSWORD").unwrap_or_else(|_| "proxima".to_string());
let dsn = format!("postgres://postgres:{password}@{host}:{port}/postgres");
let ok = futures::executor::block_on(async move {
let client = crate::Client::builder()
.pgwire(&dsn)
.build()
.expect("build client");
let response = client
.call("QUERY", "/")
.body("select 1")
.send()
.await
.expect("send select 1");
let bytes = response.bytes().await.expect("select 1 body");
let reply: Value = serde_json::from_slice(&bytes).expect("decode pgwire reply");
assert_eq!(
reply["kind"], "query",
"expected a query reply, got {reply}"
);
assert_eq!(
reply["columns"].as_array().expect("columns array").len(),
1,
"select 1 -> one column"
);
assert_eq!(
reply["rows"].as_array().expect("rows array").len(),
1,
"one row"
);
true
});
assert!(ok);
}
}