mod combine;
mod conflict;
mod cte;
mod fetch;
mod frame;
mod from;
mod group_by;
mod having;
mod join;
mod limit;
mod lock;
mod offset;
mod order_by;
mod returning;
mod select;
mod set;
mod values;
mod where_;
mod window;
mod with;
pub use combine::{Combine, Combines, HasCombines, SetOp};
pub use conflict::{
Conflict, ConflictAction, ConflictClause, ConflictTarget, HasConflict, HasConflictClause,
};
pub use cte::{Cte, CteCycle, CteSearch, SearchOrder};
pub use fetch::{Fetch, FirstOrNext, HasFetch};
pub use frame::{Frame, FrameExclusion, FrameMode, HasFrame};
pub use from::{
HasTableRef, IndexHint, IndexHintKind, IndexHintScope, IndexedBy, TableFunctions, TableRef,
};
pub use group_by::{GroupBy, GroupByWith, GroupingSet, GroupingSetKind, HasGroupBy};
pub use having::{HasHaving, Having};
pub use join::{HasJoins, Join, JoinKind};
pub use limit::{HasLimit, Limit};
pub use lock::{HasLocks, Lock, LockStrength, LockWait, Locks};
pub use offset::{HasOffset, Offset, RowsKeyword};
pub use order_by::{HasOrderBy, NullsPosition, OrderBy, OrderDef, OrderDirection};
pub use returning::{HasReturning, Returning};
pub use select::{HasSelectList, SelectList};
pub use set::{HasSet, Set};
pub use values::{HasValues, Values, ValuesRow};
pub use where_::{HasWhere, Where};
pub use window::{HasWindow, HasWindows, NamedWindow, Window, Windows};
pub use with::{HasWith, With};
use std::borrow::Cow;
use crate::writer::{Expression, SqlWriter};
trait MaybeAbsent {
fn is_absent(&self) -> bool;
}
fn write_present<E: Expression + MaybeAbsent>(
w: &mut SqlWriter<'_>,
items: &[E],
prefix: &str,
sep: &str,
suffix: &str,
) {
let mut written = false;
for item in items.iter().filter(|i| !i.is_absent()) {
w.push_str(if written { sep } else { prefix });
w.write_expr(item);
written = true;
}
if written {
w.push_str(suffix);
}
}
fn write_quoted_list(
w: &mut SqlWriter<'_>,
names: &[Cow<'static, str>],
prefix: &str,
sep: &str,
suffix: &str,
) {
if names.is_empty() {
return;
}
w.push_str(prefix);
for (i, name) in names.iter().enumerate() {
if i > 0 {
w.push_str(sep);
}
w.push_quoted(&[name]);
}
w.push_str(suffix);
}
#[cfg(test)]
mod tests {
use keelson_sqlcheck::testing::{assert_frag_sql, assert_stmt_sql};
use super::*;
use crate::dialect::testing::Numbered;
use crate::expr::{Chain, Expr, arg, quote};
use crate::value::Value;
use crate::writer::{Expression, SqlWriter, build};
#[derive(Debug, Default)]
struct Select {
with: With,
select: SelectList,
from: TableRef,
where_: Where,
group_by: GroupBy,
having: Having,
windows: Windows,
order_by: OrderBy,
limit: Limit,
locks: Locks,
combines: Combines,
}
impl Expression for Select {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.write_if(!self.with.is_empty(), "", &self.with, " ");
let tail =
!self.order_by.is_empty() || !self.limit.is_empty() || !self.locks.is_empty();
let parens = self.combines.parenthesises_leading_query(tail);
if parens {
w.push_str("(");
}
w.push_str("SELECT ");
w.write_expr(&self.select);
w.write_if(!self.from.is_empty(), " FROM ", &self.from, "");
w.write_if(!self.where_.is_empty(), " ", &self.where_, "");
w.write_if(!self.group_by.is_empty(), " ", &self.group_by, "");
w.write_if(!self.having.is_empty(), " ", &self.having, "");
w.write_if(!self.windows.is_empty(), " ", &self.windows, "");
w.write_if(!self.order_by.is_empty(), " ", &self.order_by, "");
w.write_if(!self.limit.is_empty(), " ", &self.limit, "");
w.write_if(!self.locks.is_empty(), " ", &self.locks, "");
if parens {
w.push_str(")");
}
w.write_if(!self.combines.is_empty(), " ", &self.combines, "");
}
}
fn from(table: &'static str) -> TableRef {
TableRef::new(quote(table))
}
#[test]
fn an_all_default_select_is_the_shortest_legal_statement() {
let (sql, args) = build(&Numbered, &Select::default()).unwrap();
assert_eq!(sql, "SELECT *");
assert!(args.is_empty());
}
#[test]
fn a_select_over_a_cte_joined_and_filtered() {
let inner = Select {
select: SelectList {
columns: vec![quote("id")],
..SelectList::default()
},
from: from("posts"),
..Select::default()
};
let mut with = With::default();
with.append_cte(Cte {
columns: vec!["id".into()],
..Cte::new("recent", Expr::custom(inner))
});
let mut from_users = from("users");
from_users.set_alias("u");
from_users.append_join(Join {
kind: JoinKind::Left,
to: from("recent"),
using: vec!["id".into()],
..Join::default()
});
let q = Select {
with,
select: SelectList {
columns: vec![quote(("u", "id"))],
..SelectList::default()
},
from: from_users,
where_: Where {
conditions: vec![quote(("u", "id")).eq(arg(7i32))],
},
..Select::default()
};
let (sql, args) = build(&Numbered, &q).unwrap();
assert_stmt_sql(
&sql,
concat!(
r#"WITH "recent" ("id") AS (SELECT "id" FROM "posts") "#,
r#"SELECT "u"."id" FROM "users" AS "u" LEFT JOIN "recent" USING ("id") "#,
r#"WHERE ("u"."id" = $1)"#
),
);
assert_eq!(args, vec![Value::I32(7)]);
}
#[test]
fn a_combined_select_keeps_its_own_limit_inside_the_parentheses() {
let mut combines = Combines::default();
combines.append_combine(Combine {
op: Some(SetOp::Union),
query: Some(Expr::raw("SELECT 2")),
all: true,
});
combines.order_by.append_order(Expr::raw("1"));
combines.limit.set_limit(5);
let mut limit = Limit::default();
limit.set_limit(1);
let q = Select {
select: SelectList {
columns: vec![quote("id")],
..SelectList::default()
},
from: from("users"),
limit,
combines,
..Select::default()
};
assert_stmt_sql(
&build(&Numbered, &q).unwrap().0,
r#"(SELECT "id" FROM "users" LIMIT 1) UNION ALL (SELECT 2) ORDER BY 1 LIMIT 5"#,
);
}
#[test]
fn the_same_where_mod_reaches_a_statement_and_a_conflict_clause() {
fn recent<Q: HasWhere>(q: &mut Q) {
q.where_mut().append_where(Expr::raw(r#""users"."id" > 1"#));
}
let mut select = Select {
from: from("users"),
..Select::default()
};
recent(&mut select.where_);
assert_stmt_sql(
&build(&Numbered, &select).unwrap().0,
r#"SELECT * FROM "users" WHERE "users"."id" > 1"#,
);
let mut conflict = ConflictClause {
target: ConflictTarget::on_columns(quote("id")),
..ConflictClause::do_update()
};
conflict
.set
.append_set(Expr::raw(r#""name" = EXCLUDED."name""#));
recent(&mut conflict);
recent(&mut conflict.target);
assert_eq!(
build(&Numbered, &conflict).unwrap().0,
concat!(
r#"ON CONFLICT ("id") WHERE "users"."id" > 1 "#,
r#"DO UPDATE SET "name" = EXCLUDED."name" WHERE "users"."id" > 1"#
)
);
let mut action_only = ConflictClause {
target: ConflictTarget::on_columns(quote("id")),
..ConflictClause::do_update()
};
action_only
.set
.append_set(Expr::raw(r#""name" = EXCLUDED."name""#));
recent(&mut action_only);
assert_frag_sql(
r#"INSERT INTO users ("id", "name") VALUES (1, 'kubo') {}"#,
&build(&Numbered, &action_only).unwrap().0,
r#"ON CONFLICT ("id") DO UPDATE SET "name" = EXCLUDED."name" WHERE "users"."id" > 1"#,
);
}
#[test]
fn a_quoted_list_omits_its_affixes_when_empty() {
let mut w = SqlWriter::new(&Numbered);
write_quoted_list(&mut w, &[], " (", ", ", ")");
assert_eq!(w.sql(), "");
write_quoted_list(&mut w, &["a".into(), "b".into()], " (", ", ", ")");
assert_eq!(w.sql(), r#" ("a", "b")"#);
}
}