use crate::common;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::path::PathBuf;
use std::time::Duration;
const READ_TIMEOUT: Duration = Duration::from_secs(10);
struct PgMessage {
ty: u8,
body: Vec<u8>,
}
fn read_message(s: &mut TcpStream) -> PgMessage {
let mut header = [0u8; 5];
s.read_exact(&mut header).expect("header");
let ty = header[0];
let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;
let body_len = len.saturating_sub(4);
let mut body = vec![0u8; body_len];
if body_len > 0 {
s.read_exact(&mut body).expect("body");
}
PgMessage { ty, body }
}
fn send_startup(s: &mut TcpStream, user: &str) {
let mut body = Vec::new();
body.extend_from_slice(&196_608_u32.to_be_bytes());
body.extend_from_slice(b"user\0");
body.extend_from_slice(user.as_bytes());
body.push(0);
body.push(0);
let total = u32::try_from(body.len() + 4).unwrap();
let mut out = Vec::with_capacity(body.len() + 4);
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(&body);
s.write_all(&out).unwrap();
}
fn send_query(s: &mut TcpStream, sql: &str) {
let mut body = Vec::with_capacity(sql.len() + 1);
body.extend_from_slice(sql.as_bytes());
body.push(0);
let total = u32::try_from(body.len() + 4).unwrap();
let mut out = Vec::with_capacity(body.len() + 5);
out.push(b'Q');
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(&body);
s.write_all(&out).unwrap();
}
fn read_until_ready(s: &mut TcpStream) -> Vec<PgMessage> {
let mut out = Vec::new();
loop {
let m = read_message(s);
let z = m.ty == b'Z';
out.push(m);
if z {
return out;
}
}
}
fn open(addr: &str) -> TcpStream {
let mut s = TcpStream::connect(addr).unwrap();
s.set_read_timeout(Some(READ_TIMEOUT)).unwrap();
send_startup(&mut s, "anyone");
let _ = read_until_ready(&mut s);
s
}
fn exec(s: &mut TcpStream, sql: &str) -> Vec<PgMessage> {
send_query(s, sql);
read_until_ready(s)
}
fn unique_tmpdir(label: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let p = std::env::temp_dir().join(format!("spg-e2e-r791-{label}-{nanos}"));
std::fs::create_dir_all(&p).unwrap();
p
}
fn sqlstate_of(m: &PgMessage) -> String {
let mut i = 0;
while i < m.body.len() && m.body[i] != 0 {
let code = m.body[i];
let start = i + 1;
let end = start + m.body[start..].iter().position(|&b| b == 0).unwrap();
if code == b'C' {
return String::from_utf8_lossy(&m.body[start..end]).into_owned();
}
i = end + 1;
}
String::new()
}
fn seed(s: &mut TcpStream, wide: bool) {
let val = if wide { "repeat('x', 200)" } else { "g::TEXT" };
exec(s, "CREATE TABLE t (id INT PRIMARY KEY, v TEXT)");
exec(
s,
&format!("INSERT INTO t SELECT g, {val} FROM generate_series(1,200) g"),
);
exec(s, "CREATE TABLE probe (k INT PRIMARY KEY, n INT)");
exec(s, "INSERT INTO probe VALUES (1,7)");
}
#[test]
fn midstream_division_by_zero_reports_once() {
let dir = unique_tmpdir("divzero");
let (raw, addrs) = common::ServerBuilder::new()
.arg_path(&dir.join("spg.db"))
.with_pgwire()
.spawn();
let _child = common::ChildGuard(raw);
let mut s = open(addrs.pgwire.as_ref().unwrap());
seed(&mut s, false);
let msgs = exec(
&mut s,
"SELECT (SELECT p.n FROM probe p WHERE p.k = 1) AS a, 100/(100 - t.id) AS c FROM t",
);
let errors: Vec<&PgMessage> = msgs.iter().filter(|m| m.ty == b'E').collect();
assert_eq!(errors.len(), 1, "exactly one ErrorResponse");
assert_eq!(
sqlstate_of(errors[0]),
"22012",
"division by zero, as PG 18.4 reports it"
);
assert!(
!msgs.iter().any(|m| m.ty == b'C'),
"a failed SELECT owes no CommandComplete"
);
let rows = msgs.iter().filter(|m| m.ty == b'D').count();
assert!(
rows < 100,
"at most the 99-row prefix before row 100 divides by zero, got {rows}"
);
}
#[test]
fn midstream_byte_budget_reports_once() {
let dir = unique_tmpdir("budget");
let (raw, addrs) = common::ServerBuilder::new()
.arg_path(&dir.join("spg.db"))
.with_pgwire()
.env("SPG_MAX_QUERY_BYTES", "4096")
.spawn();
let _child = common::ChildGuard(raw);
let mut s = open(addrs.pgwire.as_ref().unwrap());
seed(&mut s, true);
let msgs = exec(
&mut s,
"SELECT (SELECT p.n FROM probe p WHERE p.k = 1) AS a, t.v FROM t",
);
let errors: Vec<&PgMessage> = msgs.iter().filter(|m| m.ty == b'E').collect();
assert_eq!(
errors.len(),
1,
"the budget stops the query once, not once per path"
);
assert!(
!msgs.iter().any(|m| m.ty == b'C'),
"a budget-stopped SELECT owes no CommandComplete"
);
}