use super::{PgBytesRow, PgRow};
use crate::types::{FromPg, TypeError};
use bytes::Bytes;
#[inline]
fn column_type_meta(
column_info: &Option<std::sync::Arc<super::ColumnInfo>>,
idx: usize,
) -> Result<(u32, i16), TypeError> {
let info = column_info.as_ref().ok_or_else(|| {
TypeError::InvalidData(
"Column metadata unavailable; use query APIs that preserve RowDescription".to_string(),
)
})?;
let oid = info
.oids
.get(idx)
.copied()
.ok_or_else(|| TypeError::InvalidData(format!("Missing OID for column {}", idx)))?;
let format =
info.formats.get(idx).copied().ok_or_else(|| {
TypeError::InvalidData(format!("Missing format code for column {}", idx))
})?;
Ok((oid, format))
}
pub trait QailRow: Sized {
fn columns() -> &'static [&'static str];
fn from_row(row: &PgRow) -> Self;
}
impl PgRow {
pub fn try_get<T: FromPg>(&self, idx: usize) -> Result<T, TypeError> {
let cell = self
.columns
.get(idx)
.ok_or_else(|| TypeError::InvalidData(format!("Column index {} out of bounds", idx)))?;
let bytes = cell.as_deref().ok_or(TypeError::UnexpectedNull)?;
let (oid, format) = self.column_type_meta(idx)?;
T::from_pg(bytes, oid, format)
}
pub fn try_get_opt<T: FromPg>(&self, idx: usize) -> Result<Option<T>, TypeError> {
let cell = self
.columns
.get(idx)
.ok_or_else(|| TypeError::InvalidData(format!("Column index {} out of bounds", idx)))?;
match cell {
None => Ok(None),
Some(bytes) => {
let (oid, format) = self.column_type_meta(idx)?;
Ok(Some(T::from_pg(bytes, oid, format)?))
}
}
}
pub fn try_get_by_name<T: FromPg>(&self, name: &str) -> Result<T, TypeError> {
let idx = self
.column_index(name)
.ok_or_else(|| TypeError::InvalidData(format!("Unknown column name '{}'", name)))?;
self.try_get(idx)
}
pub fn try_get_opt_by_name<T: FromPg>(&self, name: &str) -> Result<Option<T>, TypeError> {
let idx = self
.column_index(name)
.ok_or_else(|| TypeError::InvalidData(format!("Unknown column name '{}'", name)))?;
self.try_get_opt(idx)
}
fn column_type_meta(&self, idx: usize) -> Result<(u32, i16), TypeError> {
column_type_meta(&self.column_info, idx)
}
pub fn get_string(&self, idx: usize) -> Option<String> {
self.columns
.get(idx)?
.as_ref()
.and_then(|bytes| String::from_utf8(bytes.clone()).ok())
}
pub fn get_i32(&self, idx: usize) -> Option<i32> {
if self.column_info.is_some()
&& let Ok(v) = self.try_get::<i32>(idx)
{
return Some(v);
}
let bytes = self.columns.get(idx)?.as_ref()?;
std::str::from_utf8(bytes).ok()?.parse().ok()
}
pub fn get_i64(&self, idx: usize) -> Option<i64> {
if self.column_info.is_some()
&& let Ok(v) = self.try_get::<i64>(idx)
{
return Some(v);
}
let bytes = self.columns.get(idx)?.as_ref()?;
std::str::from_utf8(bytes).ok()?.parse().ok()
}
pub fn get_f64(&self, idx: usize) -> Option<f64> {
if self.column_info.is_some()
&& let Ok(v) = self.try_get::<f64>(idx)
{
return Some(v);
}
let bytes = self.columns.get(idx)?.as_ref()?;
std::str::from_utf8(bytes).ok()?.parse().ok()
}
pub fn get_bool(&self, idx: usize) -> Option<bool> {
if self.column_info.is_some()
&& let Ok(v) = self.try_get::<bool>(idx)
{
return Some(v);
}
let bytes = self.columns.get(idx)?.as_ref()?;
let s = std::str::from_utf8(bytes).ok()?;
match s {
"t" | "true" | "1" => Some(true),
"f" | "false" | "0" => Some(false),
_ => None,
}
}
pub fn is_null(&self, idx: usize) -> bool {
self.columns.get(idx).map(|v| v.is_none()).unwrap_or(true)
}
pub fn get_bytes(&self, idx: usize) -> Option<&[u8]> {
self.columns.get(idx)?.as_ref().map(|v| v.as_slice())
}
pub fn len(&self) -> usize {
self.columns.len()
}
pub fn is_empty(&self) -> bool {
self.columns.is_empty()
}
pub fn get_uuid(&self, idx: usize) -> Option<String> {
let bytes = self.columns.get(idx)?.as_ref()?;
if bytes.len() == 16 {
use crate::protocol::types::decode_uuid;
decode_uuid(bytes).ok()
} else {
String::from_utf8(bytes.clone()).ok()
}
}
pub fn get_json(&self, idx: usize) -> Option<String> {
let bytes = self.columns.get(idx)?.as_ref()?;
if bytes.is_empty() {
return Some(String::new());
}
if bytes[0] == 1 && bytes.len() > 1 {
String::from_utf8(bytes[1..].to_vec()).ok()
} else {
String::from_utf8(bytes.clone()).ok()
}
}
pub fn get_timestamp(&self, idx: usize) -> Option<String> {
let bytes = self.columns.get(idx)?.as_ref()?;
String::from_utf8(bytes.clone()).ok()
}
pub fn get_text_array(&self, idx: usize) -> Option<Vec<String>> {
let bytes = self.columns.get(idx)?.as_ref()?;
let s = std::str::from_utf8(bytes).ok()?;
Some(crate::protocol::types::decode_text_array(s))
}
pub fn get_int_array(&self, idx: usize) -> Option<Vec<i64>> {
let bytes = self.columns.get(idx)?.as_ref()?;
let s = std::str::from_utf8(bytes).ok()?;
crate::protocol::types::decode_int_array(s).ok()
}
pub fn text(&self, idx: usize) -> String {
self.get_string(idx).unwrap_or_default()
}
pub fn text_or(&self, idx: usize, default: &str) -> String {
self.get_string(idx).unwrap_or_else(|| default.to_string())
}
pub fn int(&self, idx: usize) -> i64 {
self.get_i64(idx).unwrap_or(0)
}
pub fn float(&self, idx: usize) -> f64 {
self.get_f64(idx).unwrap_or(0.0)
}
pub fn boolean(&self, idx: usize) -> bool {
self.get_bool(idx).unwrap_or(false)
}
#[cfg(feature = "chrono")]
pub fn datetime(&self, idx: usize) -> Option<chrono::DateTime<chrono::Utc>> {
if let Ok(dt) = self.try_get::<chrono::DateTime<chrono::Utc>>(idx) {
return Some(dt);
}
let s = self.get_timestamp(idx)?;
chrono::DateTime::parse_from_rfc3339(&s.replace(' ', "T"))
.ok()
.map(|dt| dt.with_timezone(&chrono::Utc))
.or_else(|| {
chrono::DateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S%.f%#z")
.ok()
.map(|dt| dt.with_timezone(&chrono::Utc))
})
}
#[cfg(feature = "uuid")]
pub fn uuid_typed(&self, idx: usize) -> Option<uuid::Uuid> {
self.try_get::<uuid::Uuid>(idx).ok().or_else(|| {
self.get_uuid(idx)
.and_then(|s| uuid::Uuid::parse_str(&s).ok())
})
}
pub fn column_index(&self, name: &str) -> Option<usize> {
self.column_info.as_ref()?.name_to_index.get(name).copied()
}
pub fn get_string_by_name(&self, name: &str) -> Option<String> {
self.get_string(self.column_index(name)?)
}
pub fn get_i32_by_name(&self, name: &str) -> Option<i32> {
self.get_i32(self.column_index(name)?)
}
pub fn get_i64_by_name(&self, name: &str) -> Option<i64> {
self.get_i64(self.column_index(name)?)
}
pub fn get_f64_by_name(&self, name: &str) -> Option<f64> {
self.get_f64(self.column_index(name)?)
}
pub fn get_bool_by_name(&self, name: &str) -> Option<bool> {
self.get_bool(self.column_index(name)?)
}
pub fn get_uuid_by_name(&self, name: &str) -> Option<String> {
self.get_uuid(self.column_index(name)?)
}
pub fn get_json_by_name(&self, name: &str) -> Option<String> {
self.get_json(self.column_index(name)?)
}
pub fn is_null_by_name(&self, name: &str) -> bool {
self.column_index(name)
.map(|idx| self.is_null(idx))
.unwrap_or(true)
}
pub fn get_timestamp_by_name(&self, name: &str) -> Option<String> {
self.get_timestamp(self.column_index(name)?)
}
pub fn get_text_array_by_name(&self, name: &str) -> Option<Vec<String>> {
self.get_text_array(self.column_index(name)?)
}
pub fn get_int_array_by_name(&self, name: &str) -> Option<Vec<i64>> {
self.get_int_array(self.column_index(name)?)
}
pub fn text_by_name(&self, name: &str) -> String {
self.get_string_by_name(name).unwrap_or_default()
}
pub fn boolean_by_name(&self, name: &str) -> bool {
self.get_bool_by_name(name).unwrap_or(false)
}
pub fn int_by_name(&self, name: &str) -> i64 {
self.get_i64_by_name(name).unwrap_or(0)
}
pub fn float_by_name(&self, name: &str) -> f64 {
self.get_f64_by_name(name).unwrap_or(0.0)
}
#[cfg(feature = "chrono")]
pub fn datetime_by_name(&self, name: &str) -> Option<chrono::DateTime<chrono::Utc>> {
self.datetime(self.column_index(name)?)
}
#[cfg(feature = "uuid")]
pub fn uuid_typed_by_name(&self, name: &str) -> Option<uuid::Uuid> {
self.uuid_typed(self.column_index(name)?)
}
}
impl PgBytesRow {
#[inline]
pub(crate) fn release_payload(&mut self) {
self.payload = Bytes::new();
}
pub fn try_get<T: FromPg>(&self, idx: usize) -> Result<T, TypeError> {
let bytes = self.get_bytes(idx).ok_or(TypeError::UnexpectedNull)?;
let (oid, format) = column_type_meta(&self.column_info, idx)?;
T::from_pg(bytes, oid, format)
}
pub fn try_get_opt<T: FromPg>(&self, idx: usize) -> Result<Option<T>, TypeError> {
let Some(cell) = self.spans.get(idx) else {
return Err(TypeError::InvalidData(format!(
"Column index {} out of bounds",
idx
)));
};
match cell {
None => Ok(None),
Some(_) => Ok(Some(self.try_get(idx)?)),
}
}
pub fn try_get_by_name<T: FromPg>(&self, name: &str) -> Result<T, TypeError> {
let idx = self
.column_index(name)
.ok_or_else(|| TypeError::InvalidData(format!("Unknown column name '{}'", name)))?;
self.try_get(idx)
}
pub fn try_get_opt_by_name<T: FromPg>(&self, name: &str) -> Result<Option<T>, TypeError> {
let idx = self
.column_index(name)
.ok_or_else(|| TypeError::InvalidData(format!("Unknown column name '{}'", name)))?;
self.try_get_opt(idx)
}
pub fn get_bytes(&self, idx: usize) -> Option<&[u8]> {
let (start, len) = self.spans.get(idx)?.as_ref().copied()?;
self.payload.get(start..start + len)
}
pub fn for_each_column<F>(&self, mut f: F)
where
F: FnMut(usize, Option<&[u8]>),
{
for (idx, span) in self.spans.iter().enumerate() {
let value = span
.as_ref()
.and_then(|(start, len)| self.payload.get(*start..(*start + *len)));
f(idx, value);
}
}
pub fn len(&self) -> usize {
self.spans.len()
}
pub fn is_empty(&self) -> bool {
self.spans.is_empty()
}
pub fn is_null(&self, idx: usize) -> bool {
self.spans.get(idx).map(|v| v.is_none()).unwrap_or(true)
}
pub fn column_index(&self, name: &str) -> Option<usize> {
self.column_info.as_ref()?.name_to_index.get(name).copied()
}
pub fn get_i64(&self, idx: usize) -> Option<i64> {
if self.column_info.is_some()
&& let Ok(v) = self.try_get::<i64>(idx)
{
return Some(v);
}
let bytes = self.get_bytes(idx)?;
std::str::from_utf8(bytes).ok()?.parse().ok()
}
pub fn get_f64(&self, idx: usize) -> Option<f64> {
if self.column_info.is_some()
&& let Ok(v) = self.try_get::<f64>(idx)
{
return Some(v);
}
let bytes = self.get_bytes(idx)?;
std::str::from_utf8(bytes).ok()?.parse().ok()
}
pub fn get_bool(&self, idx: usize) -> Option<bool> {
if self.column_info.is_some()
&& let Ok(v) = self.try_get::<bool>(idx)
{
return Some(v);
}
let bytes = self.get_bytes(idx)?;
let s = std::str::from_utf8(bytes).ok()?;
match s {
"t" | "true" | "1" => Some(true),
"f" | "false" | "0" => Some(false),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::types::oid;
use crate::types::{Json, Uuid};
use std::collections::HashMap;
use std::sync::Arc;
fn single_col_info(name: &str, oid: u32, format: i16) -> Arc<super::super::ColumnInfo> {
let mut name_to_index = HashMap::new();
name_to_index.insert(name.to_string(), 0);
Arc::new(super::super::ColumnInfo {
name_to_index,
oids: vec![oid],
formats: vec![format],
})
}
#[test]
fn test_get_string() {
let row = PgRow {
columns: vec![Some(b"hello".to_vec()), None, Some(b"world".to_vec())],
column_info: None,
};
assert_eq!(row.get_string(0), Some("hello".to_string()));
assert_eq!(row.get_string(1), None);
assert_eq!(row.get_string(2), Some("world".to_string()));
}
#[test]
fn test_get_i32() {
let row = PgRow {
columns: vec![
Some(b"42".to_vec()),
Some(b"-123".to_vec()),
Some(b"not_a_number".to_vec()),
],
column_info: None,
};
assert_eq!(row.get_i32(0), Some(42));
assert_eq!(row.get_i32(1), Some(-123));
assert_eq!(row.get_i32(2), None);
}
#[test]
fn test_get_bool() {
let row = PgRow {
columns: vec![
Some(b"t".to_vec()),
Some(b"f".to_vec()),
Some(b"true".to_vec()),
Some(b"false".to_vec()),
],
column_info: None,
};
assert_eq!(row.get_bool(0), Some(true));
assert_eq!(row.get_bool(1), Some(false));
assert_eq!(row.get_bool(2), Some(true));
assert_eq!(row.get_bool(3), Some(false));
}
#[test]
fn test_is_null() {
let row = PgRow {
columns: vec![Some(b"value".to_vec()), None],
column_info: None,
};
assert!(!row.is_null(0));
assert!(row.is_null(1));
assert!(row.is_null(99)); }
#[test]
fn test_try_get_i64_binary() {
let row = PgRow {
columns: vec![Some(42i64.to_be_bytes().to_vec())],
column_info: Some(single_col_info("count", oid::INT8, 1)),
};
let value: i64 = row.try_get(0).unwrap();
assert_eq!(value, 42);
}
#[test]
fn test_try_get_i64_text_by_name() {
let row = PgRow {
columns: vec![Some(b"123".to_vec())],
column_info: Some(single_col_info("total", oid::INT8, 0)),
};
let value: i64 = row.try_get_by_name("total").unwrap();
assert_eq!(value, 123);
}
#[test]
fn test_try_get_opt_null() {
let row = PgRow {
columns: vec![None],
column_info: Some(single_col_info("maybe_count", oid::INT8, 1)),
};
let value: Option<i64> = row.try_get_opt(0).unwrap();
assert_eq!(value, None);
}
#[test]
fn test_try_get_unexpected_null() {
let row = PgRow {
columns: vec![None],
column_info: Some(single_col_info("required_count", oid::INT8, 1)),
};
assert!(matches!(
row.try_get::<i64>(0),
Err(TypeError::UnexpectedNull)
));
}
#[test]
fn test_try_get_uuid_binary() {
let uuid_bytes: [u8; 16] = [
0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
0x00, 0x00,
];
let row = PgRow {
columns: vec![Some(uuid_bytes.to_vec())],
column_info: Some(single_col_info("id", oid::UUID, 1)),
};
let value: Uuid = row.try_get(0).unwrap();
assert_eq!(value.0, "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_try_get_jsonb_binary() {
let mut bytes = vec![1u8];
bytes.extend_from_slice(br#"{"ok":true}"#);
let row = PgRow {
columns: vec![Some(bytes)],
column_info: Some(single_col_info("meta", oid::JSONB, 1)),
};
let value: Json = row.try_get(0).unwrap();
assert_eq!(value.0, r#"{"ok":true}"#);
}
#[test]
fn test_try_get_requires_column_metadata() {
let row = PgRow {
columns: vec![Some(b"42".to_vec())],
column_info: None,
};
assert!(matches!(
row.try_get::<i64>(0),
Err(TypeError::InvalidData(msg)) if msg.contains("metadata")
));
}
#[test]
fn test_get_i64_uses_metadata_binary() {
let row = PgRow {
columns: vec![Some(777i64.to_be_bytes().to_vec())],
column_info: Some(single_col_info("v", oid::INT8, 1)),
};
assert_eq!(row.get_i64(0), Some(777));
}
#[test]
fn test_get_bool_uses_metadata_binary() {
let row = PgRow {
columns: vec![Some(vec![1u8])],
column_info: Some(single_col_info("flag", oid::BOOL, 1)),
};
assert_eq!(row.get_bool(0), Some(true));
}
#[test]
fn test_pg_bytes_row_get_bytes() {
let row = PgBytesRow {
payload: bytes::Bytes::from_static(b"abcdef"),
spans: vec![Some((1, 3)), None],
column_info: None,
};
assert_eq!(row.get_bytes(0), Some(&b"bcd"[..]));
assert_eq!(row.get_bytes(1), None);
assert!(row.is_null(1));
}
#[test]
fn test_pg_bytes_row_try_get_i64_binary() {
let row = PgBytesRow {
payload: bytes::Bytes::from(42i64.to_be_bytes().to_vec()),
spans: vec![Some((0, 8))],
column_info: Some(single_col_info("count", oid::INT8, 1)),
};
let value: i64 = row.try_get(0).unwrap();
assert_eq!(value, 42);
}
}