use {
super::ExprNode,
crate::{
ast::{SelectItem, ToSqlUnquoted},
parse_sql::parse_select_item,
plan::SelectItemPlan,
query_builder::{ExprWithAliasNode, QueryBuilderError},
result::{Error, Result},
translate::{NO_PARAMS, translate_select_item},
},
};
#[derive(Clone, Debug)]
pub enum SelectItemNode<'a> {
SelectItem(SelectItem),
Expr(ExprNode<'a>),
Text(String),
ExprWithAliasNode(ExprWithAliasNode<'a>),
}
impl From<SelectItem> for SelectItemNode<'_> {
fn from(select_item: SelectItem) -> Self {
Self::SelectItem(select_item)
}
}
impl<'a> From<ExprNode<'a>> for SelectItemNode<'a> {
fn from(expr_node: ExprNode<'a>) -> Self {
Self::Expr(expr_node)
}
}
impl From<&str> for SelectItemNode<'_> {
fn from(select_item: &str) -> Self {
Self::Text(select_item.to_owned())
}
}
impl<'a> From<ExprWithAliasNode<'a>> for SelectItemNode<'a> {
fn from(expr_node: ExprWithAliasNode<'a>) -> Self {
Self::ExprWithAliasNode(expr_node)
}
}
impl SelectItemNode<'_> {
pub(super) fn build_select_item_plan(self) -> Result<SelectItemPlan> {
match self {
SelectItemNode::SelectItem(select_item) => Ok(select_item.into()),
SelectItemNode::Text(select_item) => parse_select_item(select_item)
.and_then(|item| translate_select_item(&item, NO_PARAMS).map(Into::into)),
SelectItemNode::Expr(expr_node) => {
let expr = expr_node
.clone()
.build_expr()
.map_err(|error| match error {
Error::QueryBuilder(
QueryBuilderError::HashJoinExecutorRequiresPlan
| QueryBuilderError::IndexByRequiresPlan,
) => QueryBuilderError::ProjectionLabelRequiresAlias.into(),
error => error,
})?;
let label = expr.to_sql_unquoted();
let expr = expr_node.build_expr_plan()?;
Ok(SelectItemPlan::Expr { expr, label })
}
SelectItemNode::ExprWithAliasNode(alias_node) => {
let (expr, label) = alias_node.build_expr_with_alias_plan()?;
Ok(SelectItemPlan::Expr { expr, label })
}
}
}
pub(super) fn build_select_item(self) -> Result<SelectItem> {
match self {
SelectItemNode::SelectItem(select_item) => Ok(select_item),
SelectItemNode::Text(select_item) => parse_select_item(select_item)
.and_then(|item| translate_select_item(&item, NO_PARAMS)),
SelectItemNode::Expr(expr_node) => {
let expr = expr_node.build_expr()?;
let label = expr.to_sql_unquoted();
Ok(SelectItem::Expr { expr, label })
}
SelectItemNode::ExprWithAliasNode(alias_node) => {
let (expr, label) = alias_node.build_expr_with_alias()?;
Ok(SelectItem::Expr { expr, label })
}
}
}
}
#[cfg(test)]
mod tests {
use {
crate::{
ast::SelectItem,
parse_sql::parse_select_item,
plan::SelectItemPlan,
query_builder::{
QueryBuilderError, SelectItemNode, col, expr, primary_key, subquery, table,
},
result::Error,
translate::{NO_PARAMS, translate_select_item},
},
pretty_assertions::assert_eq,
};
fn test(actual: SelectItemNode, expected: &str) {
let parsed = &parse_select_item(expected).expect(expected);
let expected = translate_select_item(parsed, NO_PARAMS);
assert_eq!(actual.clone().build_select_item(), expected);
assert_eq!(
actual.build_select_item_plan(),
expected.map(SelectItemPlan::from)
);
}
#[test]
fn select_item() {
let actual = SelectItem::Wildcard.into();
let expected = "*";
test(actual, expected);
let actual = "Foo.*".into();
let expected = "Foo.*";
test(actual, expected);
let actual = "id as hello".into();
let expected = "id as hello";
test(actual, expected);
let actual = col("id").into();
let expected = "id";
test(actual, expected);
let actual = col("id").alias_as("hello").into();
let expected = "id as hello";
test(actual, expected);
}
#[test]
fn plan_only_projection_expr_requires_alias_for_label() {
let actual: SelectItemNode = subquery(
table("Player")
.select()
.join("PlayerItem")
.hash_executor("PlayerItem.user_id", "Player.id"),
)
.into();
assert_eq!(
actual.build_select_item_plan(),
Err(Error::QueryBuilder(
QueryBuilderError::ProjectionLabelRequiresAlias
))
);
let actual: SelectItemNode = subquery(
table("Player")
.select()
.join("PlayerItem")
.hash_executor("PlayerItem.user_id", "Player.id"),
)
.alias_as("matched")
.into();
assert!(matches!(
actual.build_select_item_plan(),
Ok(SelectItemPlan::Expr { label, .. }) if label == "matched"
));
let actual: SelectItemNode =
subquery(table("Player").index_by(primary_key().eq("1")).select()).into();
assert_eq!(
actual.build_select_item_plan(),
Err(Error::QueryBuilder(
QueryBuilderError::ProjectionLabelRequiresAlias
))
);
let actual: SelectItemNode = subquery(
table("Player")
.index_by(primary_key().eq("1"))
.select()
.project("id"),
)
.alias_as("indexed")
.into();
assert!(matches!(
actual.build_select_item_plan(),
Ok(SelectItemPlan::Expr { label, .. }) if label == "indexed"
));
let actual: SelectItemNode = expr(")").into();
let actual = actual.build_select_item_plan().map(|_| ());
assert!(matches!(actual, Err(Error::Parser(_))));
}
}