Skip to content

Commit 5a0c77e

Browse files
committed
refactor(WIP)!: Select tokens has now a complete lifecycle on the querybuilder
1 parent 7cff81c commit 5a0c77e

13 files changed

Lines changed: 387 additions & 42 deletions

File tree

canyon_core/src/query/querybuilder/syntax/ast/select.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub struct SelectAst<'a> {
1212
pub joins: Vec<JoinClause<'a>>,
1313
pub order_by: Option<OrderByClause<'a>>,
1414
pub having: Option<HavingClause<'a>>,
15-
pub group_by: Vec<ColumnRef<'a>>,
15+
pub group_by: Option<Vec<ColumnRef<'a>>>,
1616
pub limit: Option<u64>, // TODO: strong typing
1717
pub offset: Option<u64>,
1818
}
@@ -23,7 +23,7 @@ impl<'a> SelectAst<'a> {
2323
columns: Vec::new(),
2424
joins: Vec::new(),
2525
order_by: None,
26-
group_by: Vec::new(),
26+
group_by: None,
2727
having: None,
2828
limit: None,
2929
offset: None,

canyon_core/src/query/querybuilder/syntax/clause.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::query::operators::Comp;
22
use crate::query::querybuilder::syntax::column::ColumnRef;
3+
use crate::query::querybuilder::syntax::keyword::Keyword;
34
use crate::query::querybuilder::syntax::tokens::{
45
PlaceholderKind, SqlToken, SqlTokens, ToSqlTokens,
56
};
@@ -11,21 +12,21 @@ pub struct ConditionClause<'a> {
1112
pub(crate) operator: Comp,
1213
pub(crate) value_index: usize,
1314
}
14-
#[derive(Eq, PartialEq, Clone)]
15+
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
1516
pub enum ConditionClauseKind {
1617
Where,
1718
And,
1819
Or,
1920
In,
2021
}
2122

22-
impl ConditionClauseKind {
23-
fn as_str(&self) -> &'static str {
24-
match self {
25-
ConditionClauseKind::Where => "WHERE",
26-
ConditionClauseKind::And => "AND",
27-
ConditionClauseKind::In => "IN",
28-
ConditionClauseKind::Or => "OR",
23+
impl From<ConditionClauseKind> for Keyword {
24+
fn from(keyword: ConditionClauseKind) -> Self {
25+
match keyword {
26+
ConditionClauseKind::Where => Keyword::Where,
27+
ConditionClauseKind::And => Keyword::And,
28+
ConditionClauseKind::Or => Keyword::Or,
29+
ConditionClauseKind::In => Keyword::In,
2930
}
3031
}
3132
}
@@ -35,7 +36,7 @@ impl<'a> ToSqlTokens<'a> for ConditionClause<'a> {
3536
let mut out = SqlTokens::with_capacity(4);
3637

3738
// Clause keyword
38-
out.ident(self.kind.as_str()); // NOTE: dubious
39+
out.keyword(self.kind.into());
3940

4041
// Column
4142
out.extend(self.column_name.to_tokens());

canyon_core/src/query/querybuilder/syntax/dialect.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ pub trait SqlDialect {
77
// const IDENT_QUOTING: IdentQuoting;
88
}
99

10+
pub struct StandardDialect;
11+
impl SqlDialect for StandardDialect {}
12+
1013
#[cfg(feature = "postgres")]
1114
pub struct PgDialect;
1215
#[cfg(feature = "postgres")]

canyon_core/src/query/querybuilder/syntax/emitter/types/select.rs

Lines changed: 230 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,52 @@ where
1717
) -> SqlTokens<'a> {
1818
let mut tokens = SqlTokens::default();
1919

20-
let select_ast =
21-
transient::Downcast::downcast_ref::<SelectAst>(ast.as_any()).expect("Handle this");
20+
let select_ast = transient::Downcast::downcast_ref::<SelectAst>(ast.as_any()).expect(
21+
"[emitSelect] - Handle this propagating result and introducing custom error types",
22+
);
2223

2324
tokens.keyword(Keyword::Select);
2425

2526
__impl::emit_columns(select_ast, &mut tokens);
2627
__impl::emit_from(base_ast, &mut tokens);
28+
__impl::emit_joins(select_ast, &mut tokens);
29+
__impl::emit_group_by(select_ast, &mut tokens);
30+
__impl::emit_having(select_ast, &mut tokens);
31+
__impl::emit_order_by(select_ast, &mut tokens);
32+
__impl::emit_limit(select_ast, &mut tokens);
33+
__impl::emit_offset(select_ast, &mut tokens);
2734

2835
tokens
2936
}
3037
}
3138

32-
/// DOC me this
39+
/// Emits a `SELECT` query from the provided AST nodes.
40+
///
41+
/// This trait is implemented as a blanket implementation for any type that
42+
/// implements [`SqlEmitter`]. It converts the high level [`SelectAst`] plus the
43+
/// shared [`BaseAst`] information into a sequence of [`SqlTokens`].
44+
///
45+
/// The emission process is intentionally split into small stateless helpers
46+
/// to keep the codegen predictable and allow the compiler to aggressively
47+
/// inline them.
48+
///
49+
/// Expected clause order:
50+
///
51+
/// SELECT
52+
/// -> columns
53+
/// -> FROM
54+
/// -> JOINs
55+
/// -> GROUP BY
56+
/// -> HAVING
57+
/// -> ORDER BY
58+
/// -> LIMIT
59+
/// -> OFFSET
60+
///
61+
/// `BaseAst` contains elements shared across query kinds (for example the
62+
/// root table), while `SelectAst` contains clauses specific to SELECT queries.
63+
///
64+
/// Implementors normally do not override this method and instead rely on the
65+
/// default blanket implementation.
3366
pub trait EmitSelect<'a>: SqlEmitter<'a> {
3467
fn emit_select(&mut self, ast: &impl AstProcessor<'a>, base_ast: &BaseAst<'a>)
3568
-> SqlTokens<'a>;
@@ -50,4 +83,198 @@ mod __impl {
5083
tokens.keyword(Keyword::From);
5184
tokens.extend(base_ast.table.to_tokens());
5285
}
86+
87+
pub(crate) fn emit_joins<'a>(ast: &SelectAst<'a>, tokens: &mut SqlTokens<'a>) {
88+
for join in &ast.joins {
89+
tokens.extend(join.to_tokens());
90+
}
91+
}
92+
93+
pub(crate) fn emit_group_by<'a>(ast: &SelectAst<'a>, tokens: &mut SqlTokens<'a>) {
94+
if let Some(group_by) = &ast.group_by {
95+
tokens.keyword(Keyword::GroupBy);
96+
helpers::emit_columns(&group_by, tokens);
97+
}
98+
}
99+
100+
pub(crate) fn emit_having<'a>(ast: &SelectAst<'a>, tokens: &mut SqlTokens<'a>) {
101+
if let Some(having) = &ast.having {
102+
tokens.keyword(Keyword::Having);
103+
tokens.extend(having.to_tokens());
104+
}
105+
}
106+
107+
pub(crate) fn emit_order_by<'a>(ast: &SelectAst<'a>, tokens: &mut SqlTokens<'a>) {
108+
if let Some(order_by) = &ast.order_by {
109+
tokens.extend(order_by.to_tokens());
110+
}
111+
}
112+
113+
pub(crate) fn emit_limit<'a>(ast: &SelectAst<'a>, tokens: &mut SqlTokens<'a>) {
114+
if let Some(limit) = ast.limit {
115+
tokens.keyword(Keyword::Limit);
116+
tokens.numeric(limit);
117+
}
118+
}
119+
120+
pub(crate) fn emit_offset<'a>(ast: &SelectAst<'a>, tokens: &mut SqlTokens<'a>) {
121+
if let Some(offset) = ast.offset {
122+
tokens.keyword(Keyword::Offset);
123+
tokens.numeric(offset);
124+
}
125+
}
126+
}
127+
128+
#[cfg(test)]
129+
mod tests {
130+
use crate::connection::database_type::DatabaseType;
131+
use crate::query::operators::Comp;
132+
use crate::query::querybuilder::syntax::ast::BaseAst;
133+
use crate::query::querybuilder::syntax::ast::select::SelectAst;
134+
use crate::query::querybuilder::syntax::column::ColumnRef;
135+
use crate::query::querybuilder::syntax::dialect::StandardDialect;
136+
use crate::query::querybuilder::syntax::emitter::SqlEmitter;
137+
use crate::query::querybuilder::syntax::emitter::types::select::EmitSelect;
138+
use crate::query::querybuilder::syntax::order::OrderByClause;
139+
use crate::query::querybuilder::syntax::writer::TokenWriter;
140+
141+
struct TestEmitter;
142+
impl<'a> SqlEmitter<'a> for TestEmitter {
143+
type Dialect = StandardDialect;
144+
}
145+
146+
fn col(name: &'_ str) -> ColumnRef<'_> {
147+
ColumnRef::from(name)
148+
}
149+
150+
fn render<'a>(ast: &SelectAst<'a>, base_ast: &BaseAst<'a>) -> String {
151+
let mut emitter = TestEmitter;
152+
let tokens = emitter.emit_select(ast, base_ast);
153+
TokenWriter::new()
154+
.render(&tokens, DatabaseType::Deferred)
155+
.unwrap()
156+
}
157+
158+
#[test]
159+
fn emits_select_with_columns_and_from() {
160+
let mut ast = SelectAst::new();
161+
ast.columns = vec![col("id"), col("name")];
162+
163+
let base_ast = BaseAst {
164+
table: "users".into(),
165+
..Default::default()
166+
};
167+
168+
let sql = render(&ast, &base_ast);
169+
170+
assert_eq!(sql, "SELECT id, name FROM users");
171+
}
172+
173+
#[test]
174+
fn emits_select_with_order_by_limit_and_offset() {
175+
let mut ast = SelectAst::new();
176+
ast.columns = vec![col("id")];
177+
ast.order_by = Some(OrderByClause::new("id", true));
178+
ast.limit = Some(10);
179+
ast.offset = Some(20);
180+
181+
let base_ast = BaseAst {
182+
table: "users".into(),
183+
..Default::default()
184+
};
185+
186+
let sql = render(&ast, &base_ast);
187+
188+
assert_eq!(
189+
sql,
190+
"SELECT id FROM users ORDER BY id DESC LIMIT 10 OFFSET 20"
191+
);
192+
}
193+
194+
#[test]
195+
fn emits_select_without_optional_clauses() {
196+
let mut ast = SelectAst::new();
197+
ast.columns = vec![col("*")];
198+
199+
let base_ast = BaseAst {
200+
table: "users".into(),
201+
..Default::default()
202+
};
203+
204+
let sql = render(&ast, &base_ast);
205+
206+
assert_eq!(sql, "SELECT * FROM users");
207+
}
208+
209+
#[test]
210+
fn emits_group_by_when_present() {
211+
let mut ast = SelectAst::new();
212+
ast.columns = vec![col("country")];
213+
ast.group_by = Some(vec![col("country")]);
214+
215+
let base_ast = BaseAst {
216+
table: "users".into(),
217+
..Default::default()
218+
};
219+
220+
let sql = render(&ast, &base_ast);
221+
222+
assert_eq!(sql, "SELECT country FROM users GROUP BY country");
223+
}
224+
225+
#[test]
226+
fn emits_select_with_all_join_kinds() {
227+
use crate::query::querybuilder::syntax::join::{JoinClause, JoinKind};
228+
229+
let mut ast = SelectAst::new();
230+
ast.columns = vec![col("users.id"), col("profiles.bio"), col("roles.name")];
231+
232+
ast.joins = vec![
233+
JoinClause::new(
234+
JoinKind::Inner,
235+
"profiles".into(),
236+
col("users.id"),
237+
Comp::Eq,
238+
col("profiles.user_id"),
239+
),
240+
JoinClause::new(
241+
JoinKind::Left,
242+
"roles".into(),
243+
col("users.role_id"),
244+
Comp::Eq,
245+
col("roles.id"),
246+
),
247+
JoinClause::new(
248+
JoinKind::Right,
249+
"teams".into(),
250+
col("users.team_id"),
251+
Comp::Eq,
252+
col("teams.id"),
253+
),
254+
JoinClause::new(
255+
JoinKind::FullOuter,
256+
"permissions".into(),
257+
col("users.id"),
258+
Comp::Eq,
259+
col("permissions.user_id"),
260+
),
261+
];
262+
263+
let base_ast = BaseAst {
264+
table: "users".into(),
265+
..Default::default()
266+
};
267+
268+
let sql = render(&ast, &base_ast);
269+
270+
assert_eq!(
271+
sql,
272+
"SELECT users.id, profiles.bio, roles.name \
273+
FROM users \
274+
INNER JOIN profiles ON users.id = profiles.user_id \
275+
LEFT JOIN roles ON users.role_id = roles.id \
276+
RIGHT JOIN teams ON users.team_id = teams.id \
277+
FULL OUTER JOIN permissions ON users.id = permissions.user_id"
278+
);
279+
}
53280
}

canyon_core/src/query/querybuilder/syntax/having.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use crate::query::operators::Comp;
22
use crate::query::parameters::QueryParameter;
33
use crate::query::querybuilder::syntax::column::ColumnRef;
4+
use crate::query::querybuilder::syntax::keyword::Keyword;
5+
use crate::query::querybuilder::syntax::tokens::{
6+
PlaceholderKind, SqlToken, SqlTokens, ToSqlTokens,
7+
};
48

59
pub struct HavingClause<'a> {
610
pub column: ColumnRef<'a>,
@@ -21,3 +25,16 @@ impl<'a> HavingClause<'a> {
2125
}
2226
}
2327
}
28+
29+
impl<'a> ToSqlTokens<'a> for HavingClause<'a> {
30+
fn to_tokens(&self) -> impl IntoIterator<Item = SqlToken<'a>> + 'a {
31+
let mut out = SqlTokens::with_capacity(4);
32+
33+
out.keyword(Keyword::Having);
34+
out.extend(self.column.to_tokens());
35+
out.operator(self.operator);
36+
out.placeholder(PlaceholderKind::Value(0)); // TODO: value index
37+
38+
out
39+
}
40+
}

0 commit comments

Comments
 (0)