use crate::client::GenericClient;
use crate::condition::Condition;
use crate::error::{OrmError, OrmResult};
use crate::ident::IntoIdent;
use crate::row::FromRow;
use std::sync::Arc;
use tokio_postgres::Row;
use tokio_postgres::types::{FromSql, ToSql};
#[derive(Debug)]
enum SqlPart {
Raw(String),
Param,
}
#[must_use]
pub struct Sql {
parts: Vec<SqlPart>,
params: Vec<Arc<dyn ToSql + Sync + Send>>,
tag: Option<String>,
}
#[must_use]
pub struct Query {
sql: String,
params: Vec<Arc<dyn ToSql + Sync + Send>>,
tag: Option<String>,
}
pub fn query(initial_sql: impl Into<String>) -> Query {
Query::new(initial_sql)
}
pub fn sql(initial_sql: impl Into<String>) -> Sql {
Sql::new(initial_sql)
}
fn strip_sql_prefix(sql: &str) -> &str {
let mut s = sql;
loop {
let before = s;
s = s.trim_start();
if s.starts_with("--") {
if let Some(pos) = s.find('\n') {
s = &s[pos + 1..];
continue;
}
return ""; }
if s.starts_with("/*") {
if let Some(pos) = s.find("*/") {
s = &s[pos + 2..];
continue;
}
return ""; }
if s.starts_with('(') {
s = &s[1..];
continue;
}
if s == before {
break;
}
}
s
}
fn starts_with_keyword(s: &str, keyword: &str) -> bool {
match s.get(0..keyword.len()) {
Some(prefix) => prefix.eq_ignore_ascii_case(keyword),
None => false,
}
}
impl Query {
pub fn new(sql: impl Into<String>) -> Self {
Self {
sql: sql.into(),
params: Vec::new(),
tag: None,
}
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
pub fn bind<T>(mut self, value: T) -> Self
where
T: ToSql + Sync + Send + 'static,
{
self.params.push(Arc::new(value));
self
}
pub fn sql(&self) -> &str {
&self.sql
}
pub fn params_ref(&self) -> Vec<&(dyn ToSql + Sync)> {
self.params
.iter()
.map(|p| p.as_ref() as &(dyn ToSql + Sync))
.collect()
}
pub async fn fetch_all(&self, conn: &impl GenericClient) -> OrmResult<Vec<Row>> {
let params = self.params_ref();
match self.tag.as_deref() {
Some(tag) => conn.query_tagged(tag, &self.sql, ¶ms).await,
None => conn.query(&self.sql, ¶ms).await,
}
}
pub async fn fetch_all_as<T: FromRow>(&self, conn: &impl GenericClient) -> OrmResult<Vec<T>> {
let rows = self.fetch_all(conn).await?;
rows.iter().map(T::from_row).collect()
}
pub async fn fetch_one(&self, conn: &impl GenericClient) -> OrmResult<Row> {
let params = self.params_ref();
match self.tag.as_deref() {
Some(tag) => conn.query_one_tagged(tag, &self.sql, ¶ms).await,
None => conn.query_one(&self.sql, ¶ms).await,
}
}
pub async fn fetch_one_as<T: FromRow>(&self, conn: &impl GenericClient) -> OrmResult<T> {
let row = self.fetch_one(conn).await?;
T::from_row(&row)
}
pub async fn fetch_opt(&self, conn: &impl GenericClient) -> OrmResult<Option<Row>> {
let params = self.params_ref();
match self.tag.as_deref() {
Some(tag) => conn.query_opt_tagged(tag, &self.sql, ¶ms).await,
None => conn.query_opt(&self.sql, ¶ms).await,
}
}
pub async fn fetch_opt_as<T: FromRow>(
&self,
conn: &impl GenericClient,
) -> OrmResult<Option<T>> {
let row = self.fetch_opt(conn).await?;
row.as_ref().map(T::from_row).transpose()
}
pub async fn execute(&self, conn: &impl GenericClient) -> OrmResult<u64> {
let params = self.params_ref();
match self.tag.as_deref() {
Some(tag) => conn.execute_tagged(tag, &self.sql, ¶ms).await,
None => conn.execute(&self.sql, ¶ms).await,
}
}
pub async fn fetch_all_tagged(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<Vec<Row>> {
let params = self.params_ref();
conn.query_tagged(tag, &self.sql, ¶ms).await
}
pub async fn fetch_all_tagged_as<T: FromRow>(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<Vec<T>> {
let rows = self.fetch_all_tagged(conn, tag).await?;
rows.iter().map(T::from_row).collect()
}
pub async fn fetch_one_tagged(&self, conn: &impl GenericClient, tag: &str) -> OrmResult<Row> {
let params = self.params_ref();
conn.query_one_tagged(tag, &self.sql, ¶ms).await
}
pub async fn fetch_one_tagged_as<T: FromRow>(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<T> {
let row = self.fetch_one_tagged(conn, tag).await?;
T::from_row(&row)
}
pub async fn fetch_one_strict(&self, conn: &impl GenericClient) -> OrmResult<Row> {
let params = self.params_ref();
match self.tag.as_deref() {
Some(tag) => conn.query_one_strict_tagged(tag, &self.sql, ¶ms).await,
None => conn.query_one_strict(&self.sql, ¶ms).await,
}
}
pub async fn fetch_one_strict_as<T: FromRow>(&self, conn: &impl GenericClient) -> OrmResult<T> {
let row = self.fetch_one_strict(conn).await?;
T::from_row(&row)
}
pub async fn fetch_one_strict_tagged(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<Row> {
let params = self.params_ref();
conn.query_one_strict_tagged(tag, &self.sql, ¶ms).await
}
pub async fn fetch_one_strict_tagged_as<T: FromRow>(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<T> {
let row = self.fetch_one_strict_tagged(conn, tag).await?;
T::from_row(&row)
}
pub async fn fetch_opt_tagged(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<Option<Row>> {
let params = self.params_ref();
conn.query_opt_tagged(tag, &self.sql, ¶ms).await
}
pub async fn fetch_opt_tagged_as<T: FromRow>(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<Option<T>> {
let row = self.fetch_opt_tagged(conn, tag).await?;
row.as_ref().map(T::from_row).transpose()
}
pub async fn execute_tagged(&self, conn: &impl GenericClient, tag: &str) -> OrmResult<u64> {
let params = self.params_ref();
conn.execute_tagged(tag, &self.sql, ¶ms).await
}
pub async fn fetch_scalar_one<'a, T>(&self, conn: &impl GenericClient) -> OrmResult<T>
where
T: for<'b> FromSql<'b> + Send + Sync,
{
let row = self.fetch_one(conn).await?;
row.try_get(0)
.map_err(|e| OrmError::decode("0", e.to_string()))
}
pub async fn fetch_scalar_opt<'a, T>(&self, conn: &impl GenericClient) -> OrmResult<Option<T>>
where
T: for<'b> FromSql<'b> + Send + Sync,
{
let row = self.fetch_opt(conn).await?;
match row {
Some(r) => r
.try_get(0)
.map(Some)
.map_err(|e| OrmError::decode("0", e.to_string())),
None => Ok(None),
}
}
pub async fn fetch_scalar_all<'a, T>(&self, conn: &impl GenericClient) -> OrmResult<Vec<T>>
where
T: for<'b> FromSql<'b> + Send + Sync,
{
let rows = self.fetch_all(conn).await?;
rows.iter()
.map(|r| {
r.try_get(0)
.map_err(|e| OrmError::decode("0", e.to_string()))
})
.collect()
}
pub async fn exists(&self, conn: &impl GenericClient) -> OrmResult<bool> {
let inner_sql = self.sql.trim_end();
let inner_sql = inner_sql.strip_suffix(';').unwrap_or(inner_sql).trim_end();
let trimmed = strip_sql_prefix(inner_sql);
if !starts_with_keyword(trimmed, "SELECT") && !starts_with_keyword(trimmed, "WITH") {
return Err(OrmError::Validation(
"exists() only works with SELECT statements (including WITH ... SELECT)"
.to_string(),
));
}
let wrapped_sql = format!("SELECT EXISTS({inner_sql})");
let params = self.params_ref();
let row = match self.tag.as_deref() {
Some(tag) => conn.query_one_tagged(tag, &wrapped_sql, ¶ms).await?,
None => conn.query_one(&wrapped_sql, ¶ms).await?,
};
row.try_get(0)
.map_err(|e| OrmError::decode("0", e.to_string()))
}
}
impl Sql {
pub fn new(initial_sql: impl Into<String>) -> Self {
Self {
parts: vec![SqlPart::Raw(initial_sql.into())],
params: Vec::new(),
tag: None,
}
}
pub fn empty() -> Self {
Self {
parts: Vec::new(),
params: Vec::new(),
tag: None,
}
}
pub fn tag(&mut self, tag: impl Into<String>) -> &mut Self {
self.tag = Some(tag.into());
self
}
pub fn push(&mut self, sql: &str) -> &mut Self {
if sql.is_empty() {
return self;
}
match self.parts.last_mut() {
Some(SqlPart::Raw(last)) => last.push_str(sql),
_ => self.parts.push(SqlPart::Raw(sql.to_string())),
}
self
}
pub fn push_bind<T>(&mut self, value: T) -> &mut Self
where
T: ToSql + Sync + Send + 'static,
{
self.parts.push(SqlPart::Param);
self.params.push(Arc::new(value));
self
}
pub(crate) fn push_bind_value(&mut self, value: Arc<dyn ToSql + Sync + Send>) -> &mut Self {
self.parts.push(SqlPart::Param);
self.params.push(value);
self
}
pub fn push_bind_list<T>(&mut self, values: impl IntoIterator<Item = T>) -> &mut Self
where
T: ToSql + Sync + Send + 'static,
{
let mut iter = values.into_iter();
let Some(first) = iter.next() else {
return self.push("NULL");
};
self.push_bind(first);
for v in iter {
self.push(", ");
self.push_bind(v);
}
self
}
pub fn push_sql(&mut self, mut other: Sql) -> &mut Self {
self.parts.append(&mut other.parts);
self.params.append(&mut other.params);
if self.tag.is_none() {
self.tag = other.tag;
}
self
}
pub fn push_ident<I>(&mut self, ident: I) -> OrmResult<&mut Self>
where
I: IntoIdent,
{
let ident = ident.into_ident()?;
Ok(self.push(&ident.to_sql()))
}
pub fn to_sql(&self) -> String {
let mut out = String::new();
let mut idx: usize = 0;
for part in &self.parts {
match part {
SqlPart::Raw(s) => out.push_str(s),
SqlPart::Param => {
idx += 1;
use std::fmt::Write;
let _ = write!(&mut out, "${idx}");
}
}
}
out
}
pub fn params_ref(&self) -> Vec<&(dyn ToSql + Sync)> {
self.params
.iter()
.map(|p| p.as_ref() as &(dyn ToSql + Sync))
.collect()
}
fn validate(&self) -> OrmResult<()> {
let placeholder_count = self
.parts
.iter()
.filter(|p| matches!(p, SqlPart::Param))
.count();
if placeholder_count != self.params.len() {
let params_len = self.params.len();
return Err(OrmError::Validation(format!(
"Sql: placeholders({placeholder_count}) != params({params_len})"
)));
}
Ok(())
}
pub async fn fetch_all(&self, conn: &impl GenericClient) -> OrmResult<Vec<Row>> {
self.validate()?;
let sql = self.to_sql();
let params = self.params_ref();
match self.tag.as_deref() {
Some(tag) => conn.query_tagged(tag, &sql, ¶ms).await,
None => conn.query(&sql, ¶ms).await,
}
}
pub async fn fetch_all_as<T: FromRow>(&self, conn: &impl GenericClient) -> OrmResult<Vec<T>> {
let rows = self.fetch_all(conn).await?;
rows.iter().map(T::from_row).collect()
}
pub async fn fetch_one(&self, conn: &impl GenericClient) -> OrmResult<Row> {
self.validate()?;
let sql = self.to_sql();
let params = self.params_ref();
match self.tag.as_deref() {
Some(tag) => conn.query_one_tagged(tag, &sql, ¶ms).await,
None => conn.query_one(&sql, ¶ms).await,
}
}
pub async fn fetch_one_as<T: FromRow>(&self, conn: &impl GenericClient) -> OrmResult<T> {
let row = self.fetch_one(conn).await?;
T::from_row(&row)
}
pub async fn fetch_opt(&self, conn: &impl GenericClient) -> OrmResult<Option<Row>> {
self.validate()?;
let sql = self.to_sql();
let params = self.params_ref();
match self.tag.as_deref() {
Some(tag) => conn.query_opt_tagged(tag, &sql, ¶ms).await,
None => conn.query_opt(&sql, ¶ms).await,
}
}
pub async fn fetch_opt_as<T: FromRow>(
&self,
conn: &impl GenericClient,
) -> OrmResult<Option<T>> {
let row = self.fetch_opt(conn).await?;
row.as_ref().map(T::from_row).transpose()
}
pub async fn execute(&self, conn: &impl GenericClient) -> OrmResult<u64> {
self.validate()?;
let sql = self.to_sql();
let params = self.params_ref();
match self.tag.as_deref() {
Some(tag) => conn.execute_tagged(tag, &sql, ¶ms).await,
None => conn.execute(&sql, ¶ms).await,
}
}
pub fn push_condition(&mut self, condition: &Condition) -> &mut Self {
condition.append_to_sql(self);
self
}
pub fn push_conditions_and(&mut self, conditions: &[Condition]) -> &mut Self {
for (i, cond) in conditions.iter().enumerate() {
if i > 0 {
self.push(" AND ");
}
self.push_condition(cond);
}
self
}
pub fn push_where_and(&mut self, conditions: &[Condition]) -> &mut Self {
if conditions.is_empty() {
return self;
}
self.push(" WHERE ");
self.push_conditions_and(conditions)
}
pub async fn fetch_all_tagged(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<Vec<Row>> {
self.validate()?;
let sql = self.to_sql();
let params = self.params_ref();
conn.query_tagged(tag, &sql, ¶ms).await
}
pub async fn fetch_all_tagged_as<T: FromRow>(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<Vec<T>> {
let rows = self.fetch_all_tagged(conn, tag).await?;
rows.iter().map(T::from_row).collect()
}
pub async fn fetch_one_strict(&self, conn: &impl GenericClient) -> OrmResult<Row> {
self.validate()?;
let sql = self.to_sql();
let params = self.params_ref();
match self.tag.as_deref() {
Some(tag) => conn.query_one_strict_tagged(tag, &sql, ¶ms).await,
None => conn.query_one_strict(&sql, ¶ms).await,
}
}
pub async fn fetch_one_strict_as<T: FromRow>(&self, conn: &impl GenericClient) -> OrmResult<T> {
let row = self.fetch_one_strict(conn).await?;
T::from_row(&row)
}
pub async fn fetch_one_strict_tagged(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<Row> {
self.validate()?;
let sql = self.to_sql();
let params = self.params_ref();
conn.query_one_strict_tagged(tag, &sql, ¶ms).await
}
pub async fn fetch_one_strict_tagged_as<T: FromRow>(
&self,
conn: &impl GenericClient,
tag: &str,
) -> OrmResult<T> {
let row = self.fetch_one_strict_tagged(conn, tag).await?;
T::from_row(&row)
}
pub async fn execute_tagged(&self, conn: &impl GenericClient, tag: &str) -> OrmResult<u64> {
self.validate()?;
let sql = self.to_sql();
let params = self.params_ref();
conn.execute_tagged(tag, &sql, ¶ms).await
}
pub async fn fetch_scalar_one<'a, T>(&self, conn: &impl GenericClient) -> OrmResult<T>
where
T: for<'b> FromSql<'b> + Send + Sync,
{
let row = self.fetch_one(conn).await?;
row.try_get(0)
.map_err(|e| OrmError::decode("0", e.to_string()))
}
pub async fn fetch_scalar_opt<'a, T>(&self, conn: &impl GenericClient) -> OrmResult<Option<T>>
where
T: for<'b> FromSql<'b> + Send + Sync,
{
let row = self.fetch_opt(conn).await?;
match row {
Some(r) => r
.try_get(0)
.map(Some)
.map_err(|e| OrmError::decode("0", e.to_string())),
None => Ok(None),
}
}
pub async fn fetch_scalar_all<'a, T>(&self, conn: &impl GenericClient) -> OrmResult<Vec<T>>
where
T: for<'b> FromSql<'b> + Send + Sync,
{
let rows = self.fetch_all(conn).await?;
rows.iter()
.map(|r| {
r.try_get(0)
.map_err(|e| OrmError::decode("0", e.to_string()))
})
.collect()
}
pub async fn exists(&self, conn: &impl GenericClient) -> OrmResult<bool> {
self.validate()?;
let inner_sql = self.to_sql();
let inner_sql = inner_sql.trim_end();
let inner_sql = inner_sql.strip_suffix(';').unwrap_or(inner_sql).trim_end();
let trimmed = strip_sql_prefix(inner_sql);
if !starts_with_keyword(trimmed, "SELECT") && !starts_with_keyword(trimmed, "WITH") {
return Err(OrmError::Validation(
"exists() only works with SELECT statements (including WITH ... SELECT)"
.to_string(),
));
}
let wrapped_sql = format!("SELECT EXISTS({inner_sql})");
let params = self.params_ref();
let row = match self.tag.as_deref() {
Some(tag) => conn.query_one_tagged(tag, &wrapped_sql, ¶ms).await?,
None => conn.query_one(&wrapped_sql, ¶ms).await?,
};
row.try_get(0)
.map_err(|e| OrmError::decode("0", e.to_string()))
}
pub fn limit(&mut self, n: i64) -> &mut Self {
self.push(" LIMIT ").push_bind(n)
}
pub fn offset(&mut self, n: i64) -> &mut Self {
self.push(" OFFSET ").push_bind(n)
}
pub fn limit_offset(&mut self, limit: i64, offset: i64) -> &mut Self {
self.push(" LIMIT ")
.push_bind(limit)
.push(" OFFSET ")
.push_bind(offset)
}
pub fn page(&mut self, page: i64, per_page: i64) -> OrmResult<&mut Self> {
if page < 1 {
return Err(OrmError::Validation(format!(
"page must be >= 1, got {page}"
)));
}
let offset = (page - 1) * per_page;
Ok(self.limit_offset(per_page, offset))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::condition::Condition;
async fn try_connect() -> Option<tokio_postgres::Client> {
let database_url = std::env::var("DATABASE_URL").ok()?;
let (client, connection) = tokio_postgres::connect(&database_url, tokio_postgres::NoTls)
.await
.expect("Failed to connect to DATABASE_URL with NoTls");
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("tokio-postgres connection error: {e}");
}
});
Some(client)
}
#[test]
fn builds_placeholders_in_order() {
let mut q = sql("SELECT * FROM users WHERE a = ");
q.push_bind(1).push(" AND b = ").push_bind("x");
assert_eq!(q.to_sql(), "SELECT * FROM users WHERE a = $1 AND b = $2");
assert_eq!(q.params_ref().len(), 2);
}
#[test]
fn can_compose_fragments() {
let mut w = Sql::empty();
w.push(" WHERE id = ").push_bind(42);
let mut q = sql("SELECT * FROM users");
q.push_sql(w);
assert_eq!(q.to_sql(), "SELECT * FROM users WHERE id = $1");
assert_eq!(q.params_ref().len(), 1);
}
#[test]
fn bind_list_renders_commas() {
let mut q = sql("SELECT * FROM users WHERE id IN (");
q.push_bind_list(vec![1, 2, 3]).push(")");
assert_eq!(q.to_sql(), "SELECT * FROM users WHERE id IN ($1, $2, $3)");
assert_eq!(q.params_ref().len(), 3);
}
#[test]
fn bind_list_empty_is_valid_sql() {
let mut q = sql("SELECT * FROM users WHERE id IN (");
q.push_bind_list(Vec::<i32>::new()).push(")");
assert_eq!(q.to_sql(), "SELECT * FROM users WHERE id IN (NULL)");
assert_eq!(q.params_ref().len(), 0);
}
#[test]
fn push_ident_accepts_simple_and_dotted() {
let mut q = Sql::empty();
q.push_ident("users").unwrap();
q.push(", ");
q.push_ident("public.users").unwrap();
assert_eq!(q.to_sql(), "users, public.users");
}
#[test]
fn push_ident_rejects_unsafe() {
let mut q = Sql::empty();
assert!(q.push_ident("users; drop table users; --").is_err());
assert!(q.push_ident("1users").is_err());
assert!(q.push_ident("users..name").is_err());
assert!(q.push_ident("users name").is_err());
}
#[test]
fn can_append_condition_as_placeholders() {
let mut q = sql("SELECT * FROM users WHERE ");
q.push_condition(&Condition::eq("id", 42_i64).unwrap());
assert_eq!(q.to_sql(), "SELECT * FROM users WHERE id = $1");
assert_eq!(q.params_ref().len(), 1);
}
#[test]
fn condition_placeholders_compose_with_push_bind() {
let mut q = sql("SELECT * FROM users WHERE a = ");
q.push_bind(1_i64);
q.push(" AND ");
q.push_condition(&Condition::eq("b", "x").unwrap());
assert_eq!(q.to_sql(), "SELECT * FROM users WHERE a = $1 AND b = $2");
assert_eq!(q.params_ref().len(), 2);
}
#[test]
fn empty_in_list_condition_is_valid_sql() {
let mut q = sql("SELECT * FROM users WHERE ");
q.push_condition(&Condition::in_list("id", Vec::<i32>::new()).unwrap());
assert_eq!(q.to_sql(), "SELECT * FROM users WHERE 1=0");
assert_eq!(q.params_ref().len(), 0);
}
#[test]
fn limit_appends_with_param() {
let mut q = sql("SELECT * FROM users ORDER BY id");
q.limit(10);
assert_eq!(q.to_sql(), "SELECT * FROM users ORDER BY id LIMIT $1");
assert_eq!(q.params_ref().len(), 1);
}
#[test]
fn offset_appends_with_param() {
let mut q = sql("SELECT * FROM users ORDER BY id");
q.offset(20);
assert_eq!(q.to_sql(), "SELECT * FROM users ORDER BY id OFFSET $1");
assert_eq!(q.params_ref().len(), 1);
}
#[test]
fn limit_offset_appends_both_params() {
let mut q = sql("SELECT * FROM users ORDER BY id");
q.limit_offset(10, 20);
assert_eq!(
q.to_sql(),
"SELECT * FROM users ORDER BY id LIMIT $1 OFFSET $2"
);
assert_eq!(q.params_ref().len(), 2);
}
#[test]
fn page_converts_to_limit_offset() {
let mut q = sql("SELECT * FROM users ORDER BY id");
q.page(3, 25).unwrap();
assert_eq!(
q.to_sql(),
"SELECT * FROM users ORDER BY id LIMIT $1 OFFSET $2"
);
assert_eq!(q.params_ref().len(), 2);
}
#[test]
fn page_rejects_zero() {
let mut q = sql("SELECT * FROM users ORDER BY id");
assert!(q.page(0, 25).is_err());
}
#[test]
fn page_rejects_negative() {
let mut q = sql("SELECT * FROM users ORDER BY id");
assert!(q.page(-1, 25).is_err());
}
#[test]
fn pagination_composes_with_conditions() {
let mut q = sql("SELECT * FROM users WHERE ");
q.push_condition(&Condition::eq("status", "active").unwrap());
q.push(" ORDER BY id");
q.limit_offset(10, 0);
assert_eq!(
q.to_sql(),
"SELECT * FROM users WHERE status = $1 ORDER BY id LIMIT $2 OFFSET $3"
);
assert_eq!(q.params_ref().len(), 3);
}
#[tokio::test]
async fn fetch_one_multi_rows_returns_first_row() {
let Some(client) = try_connect().await else {
eprintln!("DATABASE_URL not set; skipping");
return;
};
let row = query("SELECT n FROM (VALUES (1), (2)) AS t(n) ORDER BY n")
.fetch_one(&client)
.await
.unwrap();
let n: i32 = row.get(0);
assert_eq!(n, 1);
}
#[tokio::test]
async fn fetch_one_strict_zero_rows_is_not_found() {
let Some(client) = try_connect().await else {
eprintln!("DATABASE_URL not set; skipping");
return;
};
let err = query("SELECT 1 WHERE FALSE")
.fetch_one_strict(&client)
.await
.unwrap_err();
assert!(err.is_not_found());
}
#[tokio::test]
async fn fetch_one_strict_multi_rows_is_too_many_rows() {
let Some(client) = try_connect().await else {
eprintln!("DATABASE_URL not set; skipping");
return;
};
let err = query("SELECT n FROM (VALUES (1), (2)) AS t(n)")
.fetch_one_strict(&client)
.await
.unwrap_err();
assert!(matches!(
err,
OrmError::TooManyRows {
expected: 1,
got: 2
}
));
}
}