use std::borrow::Cow;
use crate::expr::{Expr, IntoExpr, IntoExprList};
use crate::writer::{Expression, SqlWriter};
use super::join::Join;
use super::{MaybeAbsent, write_present, write_quoted_list};
#[derive(Debug, Clone, Default)]
pub struct TableRef {
pub expression: Option<Expr>,
pub alias: Option<Cow<'static, str>>,
pub columns: Vec<Cow<'static, str>>,
pub only: bool,
pub lateral: bool,
pub with_ordinality: bool,
pub partitions: Vec<Cow<'static, str>>,
pub index_hints: Vec<IndexHint>,
pub indexed_by: Option<IndexedBy>,
pub joins: Vec<Join>,
}
impl TableRef {
pub fn new(table: impl IntoExpr) -> Self {
TableRef {
expression: Some(table.into_expr()),
..TableRef::default()
}
}
pub fn set_table(&mut self, table: impl IntoExpr) {
self.expression = Some(table.into_expr());
}
pub fn set_alias(&mut self, alias: impl Into<Cow<'static, str>>) {
self.alias = Some(alias.into());
}
pub fn set_columns(&mut self, columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) {
self.columns = columns.into_iter().map(Into::into).collect();
}
pub fn append_join(&mut self, join: Join) {
self.joins.push(join);
}
pub fn append_partition(
&mut self,
names: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) {
self.partitions.extend(names.into_iter().map(Into::into));
}
pub fn append_index_hint(&mut self, hint: IndexHint) {
self.index_hints.push(hint);
}
pub fn is_empty(&self) -> bool {
self.expression.is_none()
}
}
impl Expression for TableRef {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
let Some(expression) = &self.expression else {
return;
};
if self.only {
w.push_str("ONLY ");
}
if self.lateral {
w.push_str("LATERAL ");
}
w.write_expr(expression);
if self.with_ordinality {
w.push_str(" WITH ORDINALITY");
}
write_quoted_list(w, &self.partitions, " PARTITION (", ", ", ")");
if let Some(alias) = &self.alias {
w.push_str(" AS ");
w.push_quoted(&[alias]);
}
write_quoted_list(w, &self.columns, " (", ", ", ")");
write_present(w, &self.index_hints, " ", " ", "");
match &self.indexed_by {
None => {}
Some(IndexedBy::NotIndexed) => w.push_str(" NOT INDEXED"),
Some(IndexedBy::Index(name)) => {
w.push_str(" INDEXED BY ");
w.push_quoted(&[name]);
}
}
write_present(w, &self.joins, " ", " ", "");
}
}
pub trait HasTableRef {
fn table_ref_mut(&mut self) -> &mut TableRef;
}
impl HasTableRef for TableRef {
fn table_ref_mut(&mut self) -> &mut TableRef {
self
}
}
#[derive(Debug, Clone)]
pub enum IndexedBy {
NotIndexed,
Index(Cow<'static, str>),
}
#[derive(Debug, Clone, Default)]
pub struct IndexHint {
pub kind: Option<IndexHintKind>,
pub indexes: Vec<Cow<'static, str>>,
pub for_: Option<IndexHintScope>,
}
impl IndexHint {
pub fn new(
kind: IndexHintKind,
indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) -> Self {
IndexHint {
kind: Some(kind),
indexes: indexes.into_iter().map(Into::into).collect(),
for_: None,
}
}
pub fn is_empty(&self) -> bool {
self.kind.is_none()
}
}
impl Expression for IndexHint {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
let Some(kind) = &self.kind else {
return;
};
w.push_str(kind.as_str());
w.push_str(" INDEX");
if let Some(for_) = &self.for_ {
w.push_str(" FOR ");
w.push_str(for_.as_str());
}
w.push_str(" (");
write_quoted_list(w, &self.indexes, "", ", ", "");
w.push_str(")");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndexHintKind {
Use,
Ignore,
Force,
}
impl IndexHintKind {
pub fn as_str(self) -> &'static str {
match self {
IndexHintKind::Use => "USE",
IndexHintKind::Ignore => "IGNORE",
IndexHintKind::Force => "FORCE",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndexHintScope {
Join,
OrderBy,
GroupBy,
}
impl IndexHintScope {
pub fn as_str(self) -> &'static str {
match self {
IndexHintScope::Join => "JOIN",
IndexHintScope::OrderBy => "ORDER BY",
IndexHintScope::GroupBy => "GROUP BY",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TableFunctions {
pub functions: Vec<Expr>,
}
impl TableFunctions {
pub fn new(functions: impl IntoExprList) -> Self {
TableFunctions {
functions: functions.into_expr_list(),
}
}
pub fn is_empty(&self) -> bool {
self.functions.is_empty()
}
}
impl Expression for TableFunctions {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
if self.functions.len() > 1 {
w.write_slice(&self.functions, "ROWS FROM (", ", ", ")");
} else {
w.write_slice(&self.functions, "", ", ", "");
}
}
}
impl MaybeAbsent for IndexHint {
fn is_absent(&self) -> bool {
self.is_empty()
}
}
#[cfg(test)]
mod tests {
use keelson_sqlcheck::testing::assert_frag_sql;
use super::*;
use crate::dialect::testing::{Numbered, Positional, TestDialect};
use crate::expr::{arg, quote};
use crate::value::Value;
use crate::writer::build;
use crate::{clause::JoinKind, expr::Chain};
const FRAME: &str = "SELECT * FROM {}";
fn users() -> TableRef {
TableRef::new(quote("users"))
}
fn sql(e: &impl Expression) -> String {
build(&Numbered, e).expect("render").0
}
#[test]
fn an_empty_table_ref_writes_nothing() {
assert_eq!(build(&Numbered, &TableRef::default()).unwrap().0, "");
assert!(TableRef::default().is_empty());
}
#[test]
fn a_table_ref_with_only_decorations_still_writes_nothing() {
let t = TableRef {
only: true,
lateral: true,
alias: Some("u".into()),
..TableRef::default()
};
assert_eq!(build(&Numbered, &t).unwrap().0, "");
}
#[test]
fn a_bare_table_is_just_its_expression() {
assert_frag_sql(FRAME, &sql(&users()), r#""users""#);
}
#[test]
fn the_alias_and_its_columns_are_quoted() {
let mut t = users();
t.set_alias("u");
t.set_columns(["a", "b"]);
assert_frag_sql(FRAME, &sql(&t), r#""users" AS "u" ("a", "b")"#);
}
#[test]
fn column_aliases_without_an_alias_still_render() {
let mut t = users();
t.columns = vec!["id".into(), "name".into()];
assert_frag_sql(
"INSERT INTO {} VALUES (1, 'kubo')",
&sql(&t),
r#""users" ("id", "name")"#,
);
}
#[test]
fn postgres_decorations_bracket_the_expression() {
let only = TableRef {
only: true,
alias: Some("u".into()),
..users()
};
assert_frag_sql(FRAME, &sql(&only), r#"ONLY "users" AS "u""#);
let lateral = TableRef {
lateral: true,
with_ordinality: true,
alias: Some("x".into()),
..TableRef::new(Expr::func("generate_series", (1i32, 3i32)))
};
assert_frag_sql(
"SELECT * FROM users, {}",
&sql(&lateral),
r#"LATERAL generate_series(1, 3) WITH ORDINALITY AS "x""#,
);
}
#[test]
fn a_sub_select_in_the_from_keeps_the_outer_numbering() {
let sub = Expr::group(Expr::join((
Expr::raw(r#"SELECT "id" FROM posts WHERE "user_id" ="#),
arg(3i32),
)));
let mut t = TableRef::new(sub);
t.set_alias("p");
let (rendered, args) = build(&Numbered, &t).unwrap();
assert_frag_sql(
FRAME,
&rendered,
r#"(SELECT "id" FROM posts WHERE "user_id" = $1) AS "p""#,
);
assert_eq!(args, vec![Value::I32(3)]);
}
#[test]
fn mysql_partitions_come_before_the_alias() {
let mut t = TableRef::new(Expr::ident("users"));
t.append_partition(["p0", "p1"]);
t.set_alias("u");
assert_eq!(
build(&Positional, &t).unwrap().0,
"`users` PARTITION (`p0`, `p1`) AS `u`"
);
}
#[test]
fn index_hints_follow_the_alias_and_are_space_separated() {
let mut t = users();
t.set_alias("u");
t.append_index_hint(IndexHint::new(IndexHintKind::Use, ["a"]));
t.append_index_hint(IndexHint {
for_: Some(IndexHintScope::OrderBy),
..IndexHint::new(IndexHintKind::Ignore, ["b", "c"])
});
t.append_index_hint(IndexHint::new(
IndexHintKind::Force,
Vec::<&'static str>::new(),
));
let (sql, args) = build(&Positional, &t).unwrap();
assert_eq!(
sql,
"`users` AS `u` USE INDEX (`a`) IGNORE INDEX FOR ORDER BY (`b`, `c`) FORCE INDEX ()"
);
assert!(
args.is_empty(),
"index names are identifiers, not arguments"
);
}
#[test]
fn an_absent_hint_or_join_leaves_no_separator_behind() {
let mut t = users();
t.append_index_hint(IndexHint::default());
t.append_join(Join::default());
assert!(IndexHint::default().is_empty());
assert_frag_sql(FRAME, &sql(&t), r#""users""#);
t.append_join(Join::new(JoinKind::Cross, TableRef::new(quote("tags"))));
assert_frag_sql(FRAME, &sql(&t), r#""users" CROSS JOIN "tags""#);
}
#[test]
fn sqlite_indexed_by_has_three_states() {
let mut t = users();
assert_eq!(build(&TestDialect, &t).unwrap().0, r#""users""#);
t.indexed_by = Some(IndexedBy::NotIndexed);
assert_eq!(build(&TestDialect, &t).unwrap().0, r#""users" NOT INDEXED"#);
t.indexed_by = Some(IndexedBy::Index("users_pkey".into()));
assert_eq!(
build(&TestDialect, &t).unwrap().0,
r#""users" INDEXED BY "users_pkey""#
);
}
#[test]
fn joins_come_last_and_are_space_separated() {
let mut t = users();
t.set_alias("u");
t.append_join(Join {
kind: JoinKind::Inner,
to: TableRef::new(quote("posts")),
on: vec![quote(("u", "id")).eq(quote(("posts", "user_id")))],
..Join::default()
});
t.append_join(Join {
kind: JoinKind::Cross,
to: TableRef::new(quote("tags")),
..Join::default()
});
assert_frag_sql(
FRAME,
&sql(&t),
r#""users" AS "u" INNER JOIN "posts" ON ("u"."id" = "posts"."user_id") CROSS JOIN "tags""#,
);
}
#[test]
fn one_function_is_written_plainly_and_several_get_rows_from() {
assert_eq!(build(&Numbered, &TableFunctions::default()).unwrap().0, "");
let one = TableFunctions::new(Expr::func("generate_series", (1i32, 3i32)));
assert_frag_sql(FRAME, &sql(&one), "generate_series(1, 3)");
let many = TableFunctions::new((
Expr::func("generate_series", (1i32, 3i32)),
Expr::func("unnest", "ARRAY['a', 'b']"),
));
assert_frag_sql(
FRAME,
&sql(&many),
"ROWS FROM (generate_series(1, 3), unnest(ARRAY['a', 'b']))",
);
}
#[test]
fn a_rows_from_set_is_a_table_ref_expression() {
let mut t = TableRef::new(Expr::custom(TableFunctions::new((
Expr::func("generate_series", (1i32, 2i32)),
Expr::func("generate_series", (3i32, 4i32)),
))));
t.with_ordinality = true;
t.set_alias("x");
t.set_columns(["p", "q"]);
assert_frag_sql(
FRAME,
&sql(&t),
concat!(
r#"ROWS FROM (generate_series(1, 2), generate_series(3, 4))"#,
r#" WITH ORDINALITY AS "x" ("p", "q")"#
),
);
}
}