Skip to main content

gluesql_core/query_builder/
select_item.rs

1use {
2    super::ExprNode,
3    crate::{
4        ast::{SelectItem, ToSqlUnquoted},
5        parse_sql::parse_select_item,
6        plan::SelectItemPlan,
7        query_builder::{ExprWithAliasNode, QueryBuilderError},
8        result::{Error, Result},
9        translate::{NO_PARAMS, translate_select_item},
10    },
11};
12
13#[derive(Clone, Debug)]
14pub enum SelectItemNode<'a> {
15    SelectItem(SelectItem),
16    Expr(ExprNode<'a>),
17    Text(String),
18    ExprWithAliasNode(ExprWithAliasNode<'a>),
19}
20
21impl From<SelectItem> for SelectItemNode<'_> {
22    fn from(select_item: SelectItem) -> Self {
23        Self::SelectItem(select_item)
24    }
25}
26
27impl<'a> From<ExprNode<'a>> for SelectItemNode<'a> {
28    fn from(expr_node: ExprNode<'a>) -> Self {
29        Self::Expr(expr_node)
30    }
31}
32
33impl From<&str> for SelectItemNode<'_> {
34    fn from(select_item: &str) -> Self {
35        Self::Text(select_item.to_owned())
36    }
37}
38
39impl<'a> From<ExprWithAliasNode<'a>> for SelectItemNode<'a> {
40    fn from(expr_node: ExprWithAliasNode<'a>) -> Self {
41        Self::ExprWithAliasNode(expr_node)
42    }
43}
44
45impl SelectItemNode<'_> {
46    pub(super) fn build_select_item_plan(self) -> Result<SelectItemPlan> {
47        match self {
48            SelectItemNode::SelectItem(select_item) => Ok(select_item.into()),
49            SelectItemNode::Text(select_item) => parse_select_item(select_item)
50                .and_then(|item| translate_select_item(&item, NO_PARAMS).map(Into::into)),
51            SelectItemNode::Expr(expr_node) => {
52                let expr = expr_node
53                    .clone()
54                    .build_expr()
55                    .map_err(|error| match error {
56                        Error::QueryBuilder(
57                            QueryBuilderError::HashJoinExecutorRequiresPlan
58                            | QueryBuilderError::IndexByRequiresPlan,
59                        ) => QueryBuilderError::ProjectionLabelRequiresAlias.into(),
60                        error => error,
61                    })?;
62                let label = expr.to_sql_unquoted();
63                let expr = expr_node.build_expr_plan()?;
64
65                Ok(SelectItemPlan::Expr { expr, label })
66            }
67            SelectItemNode::ExprWithAliasNode(alias_node) => {
68                let (expr, label) = alias_node.build_expr_with_alias_plan()?;
69
70                Ok(SelectItemPlan::Expr { expr, label })
71            }
72        }
73    }
74
75    pub(super) fn build_select_item(self) -> Result<SelectItem> {
76        match self {
77            SelectItemNode::SelectItem(select_item) => Ok(select_item),
78            SelectItemNode::Text(select_item) => parse_select_item(select_item)
79                .and_then(|item| translate_select_item(&item, NO_PARAMS)),
80            SelectItemNode::Expr(expr_node) => {
81                let expr = expr_node.build_expr()?;
82                let label = expr.to_sql_unquoted();
83
84                Ok(SelectItem::Expr { expr, label })
85            }
86            SelectItemNode::ExprWithAliasNode(alias_node) => {
87                let (expr, label) = alias_node.build_expr_with_alias()?;
88
89                Ok(SelectItem::Expr { expr, label })
90            }
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use {
98        crate::{
99            ast::SelectItem,
100            parse_sql::parse_select_item,
101            plan::SelectItemPlan,
102            query_builder::{
103                QueryBuilderError, SelectItemNode, col, expr, primary_key, subquery, table,
104            },
105            result::Error,
106            translate::{NO_PARAMS, translate_select_item},
107        },
108        pretty_assertions::assert_eq,
109    };
110
111    fn test(actual: SelectItemNode, expected: &str) {
112        let parsed = &parse_select_item(expected).expect(expected);
113        let expected = translate_select_item(parsed, NO_PARAMS);
114
115        assert_eq!(actual.clone().build_select_item(), expected);
116        assert_eq!(
117            actual.build_select_item_plan(),
118            expected.map(SelectItemPlan::from)
119        );
120    }
121
122    #[test]
123    fn select_item() {
124        let actual = SelectItem::Wildcard.into();
125        let expected = "*";
126        test(actual, expected);
127
128        let actual = "Foo.*".into();
129        let expected = "Foo.*";
130        test(actual, expected);
131
132        let actual = "id as hello".into();
133        let expected = "id as hello";
134        test(actual, expected);
135
136        let actual = col("id").into();
137        let expected = "id";
138        test(actual, expected);
139
140        let actual = col("id").alias_as("hello").into();
141        let expected = "id as hello";
142        test(actual, expected);
143    }
144
145    #[test]
146    fn plan_only_projection_expr_requires_alias_for_label() {
147        let actual: SelectItemNode = subquery(
148            table("Player")
149                .select()
150                .join("PlayerItem")
151                .hash_executor("PlayerItem.user_id", "Player.id"),
152        )
153        .into();
154
155        assert_eq!(
156            actual.build_select_item_plan(),
157            Err(Error::QueryBuilder(
158                QueryBuilderError::ProjectionLabelRequiresAlias
159            ))
160        );
161
162        let actual: SelectItemNode = subquery(
163            table("Player")
164                .select()
165                .join("PlayerItem")
166                .hash_executor("PlayerItem.user_id", "Player.id"),
167        )
168        .alias_as("matched")
169        .into();
170
171        assert!(matches!(
172            actual.build_select_item_plan(),
173            Ok(SelectItemPlan::Expr { label, .. }) if label == "matched"
174        ));
175
176        let actual: SelectItemNode =
177            subquery(table("Player").index_by(primary_key().eq("1")).select()).into();
178
179        assert_eq!(
180            actual.build_select_item_plan(),
181            Err(Error::QueryBuilder(
182                QueryBuilderError::ProjectionLabelRequiresAlias
183            ))
184        );
185
186        let actual: SelectItemNode = subquery(
187            table("Player")
188                .index_by(primary_key().eq("1"))
189                .select()
190                .project("id"),
191        )
192        .alias_as("indexed")
193        .into();
194
195        assert!(matches!(
196            actual.build_select_item_plan(),
197            Ok(SelectItemPlan::Expr { label, .. }) if label == "indexed"
198        ));
199
200        let actual: SelectItemNode = expr(")").into();
201        let actual = actual.build_select_item_plan().map(|_| ());
202        assert!(matches!(actual, Err(Error::Parser(_))));
203    }
204}