1use std::borrow::Cow;
2
3use crate::expr::{Expr, IntoExpr, IntoExprList};
4use crate::writer::{Expression, SqlWriter};
5
6use super::join::Join;
7use super::{MaybeAbsent, write_present, write_quoted_list};
8
9#[derive(Debug, Clone, Default)]
33pub struct TableRef {
34 pub expression: Option<Expr>,
36
37 pub alias: Option<Cow<'static, str>>,
39 pub columns: Vec<Cow<'static, str>>,
41
42 pub only: bool,
44 pub lateral: bool,
46 pub with_ordinality: bool,
48 pub partitions: Vec<Cow<'static, str>>,
50 pub index_hints: Vec<IndexHint>,
52 pub indexed_by: Option<IndexedBy>,
54
55 pub joins: Vec<Join>,
57}
58
59impl TableRef {
60 pub fn new(table: impl IntoExpr) -> Self {
62 TableRef {
63 expression: Some(table.into_expr()),
64 ..TableRef::default()
65 }
66 }
67
68 pub fn set_table(&mut self, table: impl IntoExpr) {
70 self.expression = Some(table.into_expr());
71 }
72
73 pub fn set_alias(&mut self, alias: impl Into<Cow<'static, str>>) {
76 self.alias = Some(alias.into());
77 }
78
79 pub fn set_columns(&mut self, columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) {
81 self.columns = columns.into_iter().map(Into::into).collect();
82 }
83
84 pub fn append_join(&mut self, join: Join) {
86 self.joins.push(join);
87 }
88
89 pub fn append_partition(
91 &mut self,
92 names: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
93 ) {
94 self.partitions.extend(names.into_iter().map(Into::into));
95 }
96
97 pub fn append_index_hint(&mut self, hint: IndexHint) {
99 self.index_hints.push(hint);
100 }
101
102 pub fn is_empty(&self) -> bool {
106 self.expression.is_none()
107 }
108}
109
110impl Expression for TableRef {
111 fn write_sql(&self, w: &mut SqlWriter<'_>) {
112 let Some(expression) = &self.expression else {
113 return;
117 };
118
119 if self.only {
120 w.push_str("ONLY ");
121 }
122 if self.lateral {
123 w.push_str("LATERAL ");
124 }
125
126 w.write_expr(expression);
127
128 if self.with_ordinality {
129 w.push_str(" WITH ORDINALITY");
130 }
131
132 write_quoted_list(w, &self.partitions, " PARTITION (", ", ", ")");
133
134 if let Some(alias) = &self.alias {
135 w.push_str(" AS ");
136 w.push_quoted(&[alias]);
137 }
138 write_quoted_list(w, &self.columns, " (", ", ", ")");
141
142 write_present(w, &self.index_hints, " ", " ", "");
143
144 match &self.indexed_by {
145 None => {}
146 Some(IndexedBy::NotIndexed) => w.push_str(" NOT INDEXED"),
147 Some(IndexedBy::Index(name)) => {
148 w.push_str(" INDEXED BY ");
149 w.push_quoted(&[name]);
150 }
151 }
152
153 write_present(w, &self.joins, " ", " ", "");
154 }
155}
156
157pub trait HasTableRef {
160 fn table_ref_mut(&mut self) -> &mut TableRef;
162}
163
164impl HasTableRef for TableRef {
165 fn table_ref_mut(&mut self) -> &mut TableRef {
166 self
167 }
168}
169
170#[derive(Debug, Clone)]
176pub enum IndexedBy {
177 NotIndexed,
179 Index(Cow<'static, str>),
181}
182
183#[derive(Debug, Clone, Default)]
187pub struct IndexHint {
188 pub kind: Option<IndexHintKind>,
190 pub indexes: Vec<Cow<'static, str>>,
193 pub for_: Option<IndexHintScope>,
195}
196
197impl IndexHint {
198 pub fn new(
200 kind: IndexHintKind,
201 indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
202 ) -> Self {
203 IndexHint {
204 kind: Some(kind),
205 indexes: indexes.into_iter().map(Into::into).collect(),
206 for_: None,
207 }
208 }
209
210 pub fn is_empty(&self) -> bool {
212 self.kind.is_none()
213 }
214}
215
216impl Expression for IndexHint {
217 fn write_sql(&self, w: &mut SqlWriter<'_>) {
218 let Some(kind) = &self.kind else {
219 return;
220 };
221 w.push_str(kind.as_str());
222 w.push_str(" INDEX");
223 if let Some(for_) = &self.for_ {
224 w.push_str(" FOR ");
225 w.push_str(for_.as_str());
226 }
227 w.push_str(" (");
229 write_quoted_list(w, &self.indexes, "", ", ", "");
230 w.push_str(")");
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum IndexHintKind {
237 Use,
239 Ignore,
241 Force,
243}
244
245impl IndexHintKind {
246 pub fn as_str(self) -> &'static str {
248 match self {
249 IndexHintKind::Use => "USE",
250 IndexHintKind::Ignore => "IGNORE",
251 IndexHintKind::Force => "FORCE",
252 }
253 }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum IndexHintScope {
259 Join,
261 OrderBy,
263 GroupBy,
265}
266
267impl IndexHintScope {
268 pub fn as_str(self) -> &'static str {
270 match self {
271 IndexHintScope::Join => "JOIN",
272 IndexHintScope::OrderBy => "ORDER BY",
273 IndexHintScope::GroupBy => "GROUP BY",
274 }
275 }
276}
277
278#[derive(Debug, Clone, Default)]
293pub struct TableFunctions {
294 pub functions: Vec<Expr>,
296}
297
298impl TableFunctions {
299 pub fn new(functions: impl IntoExprList) -> Self {
301 TableFunctions {
302 functions: functions.into_expr_list(),
303 }
304 }
305
306 pub fn is_empty(&self) -> bool {
308 self.functions.is_empty()
309 }
310}
311
312impl Expression for TableFunctions {
313 fn write_sql(&self, w: &mut SqlWriter<'_>) {
314 if self.functions.len() > 1 {
315 w.write_slice(&self.functions, "ROWS FROM (", ", ", ")");
316 } else {
317 w.write_slice(&self.functions, "", ", ", "");
318 }
319 }
320}
321
322impl MaybeAbsent for IndexHint {
323 fn is_absent(&self) -> bool {
324 self.is_empty()
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use keelson_sqlcheck::testing::assert_frag_sql;
331
332 use super::*;
333 use crate::dialect::testing::{Numbered, Positional, TestDialect};
334 use crate::expr::{arg, quote};
335 use crate::value::Value;
336 use crate::writer::build;
337 use crate::{clause::JoinKind, expr::Chain};
338
339 const FRAME: &str = "SELECT * FROM {}";
341
342 fn users() -> TableRef {
343 TableRef::new(quote("users"))
344 }
345
346 fn sql(e: &impl Expression) -> String {
347 build(&Numbered, e).expect("render").0
348 }
349
350 #[test]
351 fn an_empty_table_ref_writes_nothing() {
352 assert_eq!(build(&Numbered, &TableRef::default()).unwrap().0, "");
357 assert!(TableRef::default().is_empty());
358 }
359
360 #[test]
361 fn a_table_ref_with_only_decorations_still_writes_nothing() {
362 let t = TableRef {
364 only: true,
365 lateral: true,
366 alias: Some("u".into()),
367 ..TableRef::default()
368 };
369 assert_eq!(build(&Numbered, &t).unwrap().0, "");
370 }
371
372 #[test]
373 fn a_bare_table_is_just_its_expression() {
374 assert_frag_sql(FRAME, &sql(&users()), r#""users""#);
375 }
376
377 #[test]
378 fn the_alias_and_its_columns_are_quoted() {
379 let mut t = users();
382 t.set_alias("u");
383 t.set_columns(["a", "b"]);
384 assert_frag_sql(FRAME, &sql(&t), r#""users" AS "u" ("a", "b")"#);
385 }
386
387 #[test]
388 fn column_aliases_without_an_alias_still_render() {
389 let mut t = users();
391 t.columns = vec!["id".into(), "name".into()];
392 assert_frag_sql(
393 "INSERT INTO {} VALUES (1, 'kubo')",
394 &sql(&t),
395 r#""users" ("id", "name")"#,
396 );
397 }
398
399 #[test]
400 fn postgres_decorations_bracket_the_expression() {
401 let only = TableRef {
408 only: true,
409 alias: Some("u".into()),
410 ..users()
411 };
412 assert_frag_sql(FRAME, &sql(&only), r#"ONLY "users" AS "u""#);
413
414 let lateral = TableRef {
415 lateral: true,
416 with_ordinality: true,
417 alias: Some("x".into()),
418 ..TableRef::new(Expr::func("generate_series", (1i32, 3i32)))
419 };
420 assert_frag_sql(
421 "SELECT * FROM users, {}",
422 &sql(&lateral),
423 r#"LATERAL generate_series(1, 3) WITH ORDINALITY AS "x""#,
424 );
425 }
426
427 #[test]
428 fn a_sub_select_in_the_from_keeps_the_outer_numbering() {
429 let sub = Expr::group(Expr::join((
430 Expr::raw(r#"SELECT "id" FROM posts WHERE "user_id" ="#),
431 arg(3i32),
432 )));
433 let mut t = TableRef::new(sub);
434 t.set_alias("p");
435 let (rendered, args) = build(&Numbered, &t).unwrap();
436 assert_frag_sql(
437 FRAME,
438 &rendered,
439 r#"(SELECT "id" FROM posts WHERE "user_id" = $1) AS "p""#,
440 );
441 assert_eq!(args, vec![Value::I32(3)]);
442 }
443
444 #[test]
445 fn mysql_partitions_come_before_the_alias() {
446 let mut t = TableRef::new(Expr::ident("users"));
454 t.append_partition(["p0", "p1"]);
455 t.set_alias("u");
456 assert_eq!(
457 build(&Positional, &t).unwrap().0,
458 "`users` PARTITION (`p0`, `p1`) AS `u`"
459 );
460 }
461
462 #[test]
463 fn index_hints_follow_the_alias_and_are_space_separated() {
464 let mut t = users();
468 t.set_alias("u");
469 t.append_index_hint(IndexHint::new(IndexHintKind::Use, ["a"]));
470 t.append_index_hint(IndexHint {
471 for_: Some(IndexHintScope::OrderBy),
472 ..IndexHint::new(IndexHintKind::Ignore, ["b", "c"])
473 });
474 t.append_index_hint(IndexHint::new(
475 IndexHintKind::Force,
476 Vec::<&'static str>::new(),
477 ));
478
479 let (sql, args) = build(&Positional, &t).unwrap();
480 assert_eq!(
481 sql,
482 "`users` AS `u` USE INDEX (`a`) IGNORE INDEX FOR ORDER BY (`b`, `c`) FORCE INDEX ()"
483 );
484 assert!(
485 args.is_empty(),
486 "index names are identifiers, not arguments"
487 );
488 }
489
490 #[test]
491 fn an_absent_hint_or_join_leaves_no_separator_behind() {
492 let mut t = users();
493 t.append_index_hint(IndexHint::default());
494 t.append_join(Join::default());
495 assert!(IndexHint::default().is_empty());
496 assert_frag_sql(FRAME, &sql(&t), r#""users""#);
499
500 t.append_join(Join::new(JoinKind::Cross, TableRef::new(quote("tags"))));
501 assert_frag_sql(FRAME, &sql(&t), r#""users" CROSS JOIN "tags""#);
502 }
503
504 #[test]
505 fn sqlite_indexed_by_has_three_states() {
506 let mut t = users();
510 assert_eq!(build(&TestDialect, &t).unwrap().0, r#""users""#);
511
512 t.indexed_by = Some(IndexedBy::NotIndexed);
513 assert_eq!(build(&TestDialect, &t).unwrap().0, r#""users" NOT INDEXED"#);
514
515 t.indexed_by = Some(IndexedBy::Index("users_pkey".into()));
516 assert_eq!(
517 build(&TestDialect, &t).unwrap().0,
518 r#""users" INDEXED BY "users_pkey""#
519 );
520 }
521
522 #[test]
523 fn joins_come_last_and_are_space_separated() {
524 let mut t = users();
525 t.set_alias("u");
526 t.append_join(Join {
527 kind: JoinKind::Inner,
528 to: TableRef::new(quote("posts")),
529 on: vec![quote(("u", "id")).eq(quote(("posts", "user_id")))],
530 ..Join::default()
531 });
532 t.append_join(Join {
533 kind: JoinKind::Cross,
534 to: TableRef::new(quote("tags")),
535 ..Join::default()
536 });
537
538 assert_frag_sql(
539 FRAME,
540 &sql(&t),
541 r#""users" AS "u" INNER JOIN "posts" ON ("u"."id" = "posts"."user_id") CROSS JOIN "tags""#,
542 );
543 }
544
545 #[test]
546 fn one_function_is_written_plainly_and_several_get_rows_from() {
547 assert_eq!(build(&Numbered, &TableFunctions::default()).unwrap().0, "");
550
551 let one = TableFunctions::new(Expr::func("generate_series", (1i32, 3i32)));
552 assert_frag_sql(FRAME, &sql(&one), "generate_series(1, 3)");
553
554 let many = TableFunctions::new((
555 Expr::func("generate_series", (1i32, 3i32)),
556 Expr::func("unnest", "ARRAY['a', 'b']"),
557 ));
558 assert_frag_sql(
559 FRAME,
560 &sql(&many),
561 "ROWS FROM (generate_series(1, 3), unnest(ARRAY['a', 'b']))",
562 );
563 }
564
565 #[test]
566 fn a_rows_from_set_is_a_table_ref_expression() {
567 let mut t = TableRef::new(Expr::custom(TableFunctions::new((
568 Expr::func("generate_series", (1i32, 2i32)),
569 Expr::func("generate_series", (3i32, 4i32)),
570 ))));
571 t.with_ordinality = true;
572 t.set_alias("x");
573 t.set_columns(["p", "q"]);
574 assert_frag_sql(
575 FRAME,
576 &sql(&t),
577 concat!(
578 r#"ROWS FROM (generate_series(1, 2), generate_series(3, 4))"#,
579 r#" WITH ORDINALITY AS "x" ("p", "q")"#
580 ),
581 );
582 }
583}