Skip to main content

keelson_core/clause/
from.rs

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/// A `from_item`: a table, a sub-select, a function call or a CTE name, plus every
10/// decoration the three dialects hang off one, plus its joins.
11///
12/// ```text
13/// [ONLY] [LATERAL] expr [WITH ORDINALITY] [PARTITION (…)] [[AS] alias [(cols)]]
14///        [index hints] [INDEXED BY … | NOT INDEXED] [joins]
15/// ```
16///
17/// The order is PostgreSQL's `from_item` production
18/// (<https://www.postgresql.org/docs/17/sql-select.html>) with MySQL's
19/// `table_reference` (<https://dev.mysql.com/doc/refman/8.4/en/join.html>) and
20/// SQLite's `qualified-table-name` slotted into the positions those grammars put
21/// them in.
22///
23/// Every dialect's decorations live on this one struct rather than in three
24/// near-identical copies, because a shared [`Join`] has to be able to hold a
25/// `TableRef` whatever dialect it came from. Which of them a dialect *permits* is
26/// decided by which mods that dialect exports — not by this shape. The
27/// dialect-specific fields are labelled below.
28///
29/// The same struct is also an `INSERT`'s target, where
30/// [`columns`](Self::columns) is the insert column list, and an `UPDATE`'s or
31/// `DELETE`'s table.
32#[derive(Debug, Clone, Default)]
33pub struct TableRef {
34    /// The table, sub-select, function call or CTE name.
35    pub expression: Option<Expr>,
36
37    /// The table alias, quoted on output.
38    pub alias: Option<Cow<'static, str>>,
39    /// Column aliases — or, for an `INSERT`, the insert column list. Quoted.
40    pub columns: Vec<Cow<'static, str>>,
41
42    /// PostgreSQL `ONLY`: do not include descendant tables.
43    pub only: bool,
44    /// PostgreSQL and MySQL `LATERAL`.
45    pub lateral: bool,
46    /// PostgreSQL `WITH ORDINALITY`, for a set-returning function.
47    pub with_ordinality: bool,
48    /// MySQL `PARTITION (…)`. Quoted.
49    pub partitions: Vec<Cow<'static, str>>,
50    /// MySQL `USE`/`FORCE`/`IGNORE INDEX`.
51    pub index_hints: Vec<IndexHint>,
52    /// SQLite `INDEXED BY …` / `NOT INDEXED`.
53    pub indexed_by: Option<IndexedBy>,
54
55    /// Joins hanging off this item, rendered after all of its decorations.
56    pub joins: Vec<Join>,
57}
58
59impl TableRef {
60    /// A plain table reference with no decorations.
61    pub fn new(table: impl IntoExpr) -> Self {
62        TableRef {
63            expression: Some(table.into_expr()),
64            ..TableRef::default()
65        }
66    }
67
68    /// Replace the table.
69    pub fn set_table(&mut self, table: impl IntoExpr) {
70        self.expression = Some(table.into_expr());
71    }
72
73    /// Set the alias. Column aliases are [`set_columns`](Self::set_columns), so
74    /// that neither has to be named to set the other.
75    pub fn set_alias(&mut self, alias: impl Into<Cow<'static, str>>) {
76        self.alias = Some(alias.into());
77    }
78
79    /// Set the column aliases — or, for an `INSERT`, the insert column list.
80    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    /// Append a join.
85    pub fn append_join(&mut self, join: Join) {
86        self.joins.push(join);
87    }
88
89    /// Append MySQL partition names.
90    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    /// Append a MySQL index hint.
98    pub fn append_index_hint(&mut self, hint: IndexHint) {
99        self.index_hints.push(hint);
100    }
101
102    /// Whether there is no table at all — what a query tests before writing
103    /// `FROM`. A `TableRef` that has joins but no table of its own is still
104    /// unwritable, so only the table is consulted.
105    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            // No table: nothing can be written, not even the decorations, and the
114            // enclosing statement is the one that knows whether that is legal
115            // (`DELETE` needs a table, `SELECT 1` does not).
116            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        // Column aliases can appear without a table alias — `t (a, b)` — and an
139        // INSERT's column list always does.
140        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
157/// Anything with a table reference: the `FROM` of a `SELECT`, the target of an
158/// `INSERT`/`UPDATE`/`DELETE`, the `USING` of a `DELETE`.
159pub trait HasTableRef {
160    /// The table reference to modify.
161    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/// SQLite's index directive.
171///
172/// Three states rather than bob's `*string` with `""` standing for `NOT INDEXED`:
173/// an empty index name is not a thing SQLite has a syntax for, so it should not be
174/// representable.
175#[derive(Debug, Clone)]
176pub enum IndexedBy {
177    /// `NOT INDEXED` — refuse the index the planner would have chosen.
178    NotIndexed,
179    /// `INDEXED BY <name>`.
180    Index(Cow<'static, str>),
181}
182
183/// MySQL's `USE | FORCE | IGNORE INDEX [FOR …] (indexes)`.
184///
185/// Never contributes a bound argument: index names are identifiers.
186#[derive(Debug, Clone, Default)]
187pub struct IndexHint {
188    /// Which hint. `None` is how a default-constructed hint stays absent.
189    pub kind: Option<IndexHintKind>,
190    /// The index names, quoted. May be empty — `USE INDEX ()` is how MySQL is
191    /// told to use no index at all, so the parentheses are unconditional.
192    pub indexes: Vec<Cow<'static, str>>,
193    /// Restrict the hint to one phase of planning.
194    pub for_: Option<IndexHintScope>,
195}
196
197impl IndexHint {
198    /// A hint of `kind` over `indexes`, applying to the whole query.
199    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    /// Whether the hint is absent.
211    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        // Unconditional, empty list included: see `indexes`.
228        w.push_str(" (");
229        write_quoted_list(w, &self.indexes, "", ", ", "");
230        w.push_str(")");
231    }
232}
233
234/// Which way a MySQL index hint leans.
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum IndexHintKind {
237    /// `USE INDEX` — prefer these.
238    Use,
239    /// `IGNORE INDEX` — do not consider these.
240    Ignore,
241    /// `FORCE INDEX` — a table scan is not acceptable.
242    Force,
243}
244
245impl IndexHintKind {
246    /// The keyword, as written.
247    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/// What a MySQL index hint applies to.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum IndexHintScope {
259    /// `FOR JOIN`.
260    Join,
261    /// `FOR ORDER BY`.
262    OrderBy,
263    /// `FOR GROUP BY`.
264    GroupBy,
265}
266
267impl IndexHintScope {
268    /// The keyword, as written.
269    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/// A set of set-returning functions in a `FROM`, which PostgreSQL spells
279/// `ROWS FROM (…)` once there is more than one.
280///
281/// ```text
282/// [LATERAL] function_name ( … ) [WITH ORDINALITY] …
283/// [LATERAL] ROWS FROM ( function_name ( … ) [AS (coldefs)] [, …] ) [WITH ORDINALITY] …
284/// ```
285///
286/// The rule worth keeping is that the wrapper appears **only** for a set: one
287/// function is written plainly, because `ROWS FROM (f())` and `f()` mean the same
288/// thing and the shorter form is what a person writes. Put the result in
289/// [`TableRef::expression`]; the column-definition lists belong to the individual
290/// function expressions, which is a dialect's own function builder reaching core
291/// through [`Expr::Custom`](crate::expr::Expr::Custom).
292#[derive(Debug, Clone, Default)]
293pub struct TableFunctions {
294    /// The function calls, in order.
295    pub functions: Vec<Expr>,
296}
297
298impl TableFunctions {
299    /// A set from any expression list.
300    pub fn new(functions: impl IntoExprList) -> Self {
301        TableFunctions {
302            functions: functions.into_expr_list(),
303        }
304    }
305
306    /// Whether there are no functions at all.
307    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    /// A table reference is a fragment of a `FROM`.
340    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        // Not framed: `SELECT * FROM` is not a statement, so there is nothing to
353        // judge — which is precisely why a query type has to omit the keyword
354        // along with the reference. `SELECT * FROM users {}` would judge the frame
355        // and say nothing about the fragment.
356        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        // No table means nothing is writable — ONLY on its own is not SQL.
363        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        // PostgreSQL 17 from_item: `table_name [ [ AS ] alias [ ( column_alias
380        // [, ...] ) ] ]` — fewer column aliases than columns renames a prefix.
381        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        // This is the INSERT column-list shape: `INSERT INTO users ("id", "name")`.
390        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        // PostgreSQL 17 from_item:
402        //   [ ONLY ] table_name … | [ LATERAL ] function_name ( … )
403        //   [ WITH ORDINALITY ] [ [ AS ] alias … ]
404        // ONLY and LATERAL precede the item; WITH ORDINALITY follows it and
405        // precedes the alias. ONLY qualifies a table and LATERAL a function, so
406        // they are judged on the item each one is legal on rather than together.
407        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        // MySQL 8.4 table_reference:
447        //   tbl_name [PARTITION (partition_names)] [[AS] alias] [index_hint_list]
448        //
449        // Not framed: this is MySQL syntax rendered by the MySQL-shaped stand-in
450        // dialect, and the judge reachable from here is PostgreSQL's — which has
451        // neither backticks nor PARTITION. `keelson-mysql` checks the same shape
452        // against a real MySQL.
453        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        // MySQL 8.4: index_hint_list follows the alias, and each hint always
465        // brings its parentheses — `USE INDEX ()` is meaningful. MySQL-only, so
466        // unframed for the same reason as the case above.
467        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        // Not even the space that would precede them: an absent item is absent
497        // separator and all.
498        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        // SQLite's own `INDEXED BY` / `NOT INDEXED`. Not framed: PostgreSQL has no
507        // such syntax, so the judge reachable from here would reject valid SQL.
508        // `keelson-sqlite` checks it against a real SQLite.
509        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        // PostgreSQL 17 from_item: the ROWS FROM( … ) form exists to hold a *list*
548        // of function calls; a single call is a from_item on its own.
549        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}