Skip to main content

inillucent_sql/bind/
cte.rs

1//! Common table expressions: what a `WITH` binds, and how a recursive one is
2//! filled.
3//!
4//! Invariant: **a CTE is bound once per reference and never bound inside
5//! itself.** Two references to one CTE are two independent scans with their
6//! own FROM-term numbers, which is why a binding holds an AST id rather than a
7//! bound block; and a definition already being bound is a cycle, which is
8//! answered rather than followed.
9//!
10//! ## Why this is its own module
11//!
12//! `bind.rs` was at its recorded ceiling and task-1913 added ninety-nine lines
13//! to it, so the ratchet in `policy.rs` asked for an extraction rather than a
14//! raised number. This is one question - what a name in a `WITH` stands for -
15//! and the ten items here were the only ones asking it. Nothing moved changed
16//! in the move.
17
18use super::{subquery_table, unsupported, Binder, BoundSource, RecursiveBody, SourceRows};
19use crate::ast::{self, CompoundOp, JoinKind, SelectId};
20use crate::catalog_view::TableInfo;
21use crate::diagnostic::{ParseError, ParseErrorKind};
22use crate::lexer::Span;
23
24/// The first number a derived table inside a correlated subquery keeps its rows
25/// under, which is past any number a common table expression can have.
26pub const FIRST_ANONYMOUS_SHARED: usize = 1 << 20;
27
28/// One common table expression visible to a block.
29///
30/// The definition is kept as an AST id rather than a bound block because two
31/// references to the same CTE are two independent scans: each gets its own
32/// FROM-term numbers and its own materialisation. Binding once and cloning
33/// would give both references the same source ids, and the second scan would
34/// then read the first one's cursors.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct CteBinding {
37    /// The folded name a FROM term matches against.
38    pub folded: Vec<u8>,
39    /// The name as written, which the expansion is aliased to.
40    pub name: Vec<u8>,
41    /// The explicit column list, when the `WITH` wrote one.
42    pub columns: Vec<Vec<u8>>,
43    /// The query the name stands for.
44    pub select: SelectId,
45    /// Whether the `WITH` said `RECURSIVE`.
46    pub recursive: bool,
47    /// `Some(true)` for `MATERIALIZED`, `Some(false)` for `NOT MATERIALIZED`.
48    pub materialized: Option<bool>,
49    /// The position of the `WITH` that defined it in the stack of `WITH`
50    /// levels, which is how far down the body of the definition may look.
51    pub level: usize,
52}
53
54/// One recursive CTE whose definition is being bound.
55#[derive(Clone, Debug)]
56pub(super) struct RecursiveTarget {
57    /// The CTE's folded name.
58    pub(super) folded: Vec<u8>,
59    /// The statement-wide number of the FROM term that will hold its store.
60    id: usize,
61    /// The columns a reference to it exposes, taken from the seed arm.
62    table: TableInfo,
63    /// Whether any arm bound so far referred to it.
64    referenced: bool,
65}
66
67impl Binder<'_> {
68    /// Pushes the CTEs of a `WITH` prefix, returning whether it pushed any.
69    pub(crate) fn push_ctes(&mut self, with: &ast::With) -> Result<bool, ParseError> {
70        if with.ctes.is_empty() {
71            return Ok(false);
72        }
73        let mut bindings = Vec::with_capacity(with.ctes.len());
74        for cte in &with.ctes {
75            // **A name may be defined once in one `WITH`.** SQLite refuses the
76            // second definition while it parses, in these words; an inner
77            // `WITH` is a different clause and may reuse the name.
78            let folded = self.ast.folded(cte.name);
79            if bindings
80                .iter()
81                .any(|held: &CteBinding| held.folded.as_slice() == folded)
82            {
83                return Err(super::refused(
84                    format!(
85                        "duplicate WITH table name: {}",
86                        String::from_utf8_lossy(self.ast.text(cte.name))
87                    ),
88                    crate::lexer::Span::default(),
89                ));
90            }
91            bindings.push(CteBinding {
92                folded: self.ast.folded(cte.name).to_vec(),
93                name: self.ast.text(cte.name).to_vec(),
94                columns: cte
95                    .columns
96                    .iter()
97                    .map(|name| self.ast.text(*name).to_vec())
98                    .collect(),
99                select: cte.select,
100                recursive: with.recursive,
101                materialized: cte.materialized,
102                level: self.ctes.len(),
103            });
104        }
105        self.ctes.push(bindings);
106        Ok(true)
107    }
108
109    /// Drops the innermost level of CTE bindings.
110    pub(crate) fn pop_ctes(&mut self) {
111        self.ctes.pop();
112    }
113
114    /// Returns the innermost CTE a folded name matches.
115    pub(super) fn find_cte(&self, folded: &[u8]) -> Option<CteBinding> {
116        for level in self.ctes.iter().rev() {
117            if let Some(found) = level.iter().find(|cte| cte.folded == folded) {
118                return Some(found.clone());
119            }
120        }
121        None
122    }
123
124    /// Reports whether the statement refers to a name in more than one FROM term.
125    ///
126    /// Counted over every FROM term the statement was parsed into, so an inner
127    /// `WITH` that reuses the name is counted too. That only ever shares a CTE
128    /// that did not need to be shared.
129    ///
130    /// @param folded - the folded name
131    pub(super) fn name_is_used_twice(&self, folded: &[u8]) -> bool {
132        self.name_uses(folded) > 1
133    }
134
135    /// Counts the FROM terms of the statement that name a table.
136    ///
137    /// Counted over every FROM term the statement was parsed into, as
138    /// [`Binder::name_is_used_twice`] counts them.
139    ///
140    /// @param folded - the folded name
141    pub(super) fn name_uses(&self, folded: &[u8]) -> u32 {
142        let mut uses = 0u32;
143        for at in 0..self.ast.from_term_count() {
144            let Some(term) = self.ast.from_term(ast::FromTermId(at as u32)) else {
145                continue;
146            };
147            if let ast::FromSource::Table {
148                database: None,
149                name,
150                ..
151            } = &term.source
152            {
153                if self.ast.folded(*name) == folded {
154                    uses = uses.saturating_add(1);
155                }
156            }
157        }
158        uses
159    }
160
161    /// Marks the block a CTE reference was just bound to as one that shares its
162    /// evaluation with the other references, when that can be seen.
163    ///
164    /// SQLite evaluates a CTE used more than once a single time unless it is
165    /// `NOT MATERIALIZED`. The difference can only be seen when the body is not
166    /// a function of its tables, so only a body that calls `random()` or a
167    /// function like it is marked, and one that reads a column of an enclosing
168    /// query is left alone because it has a different answer for every row.
169    ///
170    /// @param cte - the definition
171    pub(super) fn share_last_source(&mut self, cte: &CteBinding) {
172        if cte.materialized == Some(false) || !self.name_is_used_twice(&cte.folded) {
173            return;
174        }
175        let key = match self.shared_ctes.iter().position(|(arena, select)| {
176            *arena == self.ast as *const _ as usize && *select == cte.select
177        }) {
178            Some(key) => key,
179            None => {
180                self.shared_ctes
181                    .push((self.ast as *const _ as usize, cte.select));
182                self.shared_ctes.len().saturating_sub(1)
183            }
184        };
185        let Some(source) = self.sources.last_mut() else {
186            return;
187        };
188        let SourceRows::Subquery(block) = &mut source.rows else {
189            return;
190        };
191        if !block.correlations.is_empty() {
192            return;
193        }
194        let mut volatile = false;
195        let mut probe = (**block).clone();
196        crate::rewrite::rewrite_select(&mut probe, &mut |expr: &mut super::BoundExpr| {
197            if crate::plan::calls_a_volatile_function(expr) {
198                volatile = true;
199            }
200        });
201        if volatile {
202            block.shared = Some(key);
203        }
204    }
205
206    /// Makes the derived tables of a correlated subquery that read nothing of
207    /// the enclosing query keep their rows for the whole statement.
208    ///
209    /// **SQLite materialises such a derived table once** (`OP_Once`), however
210    /// many outer rows the subquery runs for. Read again for every row, a
211    /// `SELECT ... FROM (SELECT sum(v) OVER () ...)` inside an `UPDATE`'s `SET`
212    /// saw the rows the statement had already changed.
213    ///
214    /// @param block - a subquery that reads a column of an enclosing query
215    pub(super) fn share_uncorrelated_sources(&mut self, block: &mut super::BoundSelect) {
216        for source in &mut block.sources {
217            let SourceRows::Subquery(inner) = &mut source.rows else {
218                continue;
219            };
220            if inner.correlations.is_empty() {
221                if inner.shared.is_none() {
222                    inner.shared = Some(FIRST_ANONYMOUS_SHARED + self.shared_anonymous);
223                    self.shared_anonymous = self.shared_anonymous.saturating_add(1);
224                }
225            } else {
226                self.share_uncorrelated_sources(inner);
227            }
228        }
229        for (_, arm) in &mut block.compounds {
230            self.share_uncorrelated_sources(arm);
231        }
232    }
233
234    /// Reports whether a CTE's own query names it in a FROM clause.
235    ///
236    /// **What makes a CTE recursive is the self-reference, not the keyword.**
237    /// SQLite accepts `WITH c AS (SELECT 1 UNION ALL SELECT ... FROM c)` with
238    /// no `RECURSIVE` written and answers it; this binder read only the
239    /// keyword, so the same query bound `c`'s definition inside `c`'s
240    /// definition until the process ran out of stack (task-1913).
241    ///
242    /// An inner `WITH` that binds the same name shadows the outer one, so
243    /// nothing under it can be the recursion - which is why this stops there
244    /// rather than reporting every mention of the name.
245    ///
246    /// @param select - the CTE's query
247    /// @param folded - the CTE's folded name
248    pub(super) fn select_names_itself(&self, select: ast::SelectId, folded: &[u8]) -> bool {
249        let Some(query) = self.ast.select(select) else {
250            return false;
251        };
252        if query
253            .with
254            .ctes
255            .iter()
256            .any(|inner| self.ast.folded(inner.name) == folded)
257        {
258            return false;
259        }
260        if self.core_names_cte(query.first, folded) {
261            return true;
262        }
263        query
264            .compounds
265            .iter()
266            .any(|(_, arm)| self.core_names_cte(*arm, folded))
267    }
268
269    /// Reports whether one arm of a compound names a CTE in its FROM clause.
270    ///
271    /// @param core - the arm
272    /// @param folded - the CTE's folded name
273    pub(super) fn core_names_cte(&self, core: ast::SelectCoreId, folded: &[u8]) -> bool {
274        let Some(arm) = self.ast.core(core) else {
275            return false;
276        };
277        let ast::SelectBody::Select { from, .. } = &arm.body else {
278            return false;
279        };
280        self.terms_name_cte(from, folded)
281    }
282
283    /// Reports whether any FROM term names a CTE.
284    ///
285    /// @param terms - the FROM terms
286    /// @param folded - the CTE's folded name
287    pub(super) fn terms_name_cte(&self, terms: &[ast::FromTermId], folded: &[u8]) -> bool {
288        terms.iter().any(|id| match self.ast.from_term(*id) {
289            Some(term) => match &term.source {
290                ast::FromSource::Table { database, name, .. } => {
291                    database.is_none() && self.ast.folded(*name) == folded
292                }
293                ast::FromSource::Subquery(select) => self.select_names_itself(*select, folded),
294                ast::FromSource::Join(inner) => self.terms_name_cte(inner, folded),
295            },
296            None => false,
297        })
298    }
299
300    /// Registers a reference to the recursive CTE currently being bound.
301    pub(super) fn push_recursive_self(
302        &mut self,
303        position: usize,
304        alias: Option<ast::NameId>,
305        join: JoinKind,
306    ) -> Result<(), ParseError> {
307        let Some(target) = self.recursing.get_mut(position) else {
308            return Err(unsupported("unknown recursive reference", Span::default()));
309        };
310        target.referenced = true;
311        let cte = target.id;
312        let table = target.table.clone();
313        let alias = match alias {
314            Some(alias) => self.ast.text(alias).to_vec(),
315            None => table.name.clone(),
316        };
317        let id = self.sources.len();
318        self.sources.push(BoundSource {
319            index_hint: crate::bind::IndexChoice::Any,
320            id,
321            rows: SourceRows::RecursiveSelf { cte },
322            table: std::rc::Rc::new(table),
323            alias,
324            join,
325            constraint: None,
326            suppressed: Vec::new(),
327            index_exprs: Vec::new(),
328            written_schema: None,
329            derived: Default::default(),
330        });
331        if let Some(scope) = self.scopes.last_mut() {
332            scope.push(id);
333        }
334        Ok(())
335    }
336
337    /// Binds a `WITH RECURSIVE` CTE reference.
338    ///
339    /// The seed arm is bound first, alone, because until it is bound nothing
340    /// knows what columns the CTE has - and the step arm cannot be bound until
341    /// a reference to the CTE has columns to resolve against. A CTE declared
342    /// `RECURSIVE` that turns out not to reference itself is an ordinary
343    /// compound, and is rebuilt as one rather than run through a queue that
344    /// would never be fed.
345    pub(super) fn bind_recursive_cte(
346        &mut self,
347        cte: &CteBinding,
348        alias: Vec<u8>,
349        join: JoinKind,
350        span: Span,
351    ) -> Result<(), ParseError> {
352        let Some(select) = self.ast.select(cte.select) else {
353            return Err(unsupported("missing select", span));
354        };
355        if select.compounds.is_empty() {
356            return self.bind_subquery_term(
357                cte.select,
358                Some(alias),
359                cte.columns.clone(),
360                join,
361                span,
362            );
363        }
364        let arms: Vec<(CompoundOp, ast::SelectCoreId)> = select.compounds.clone();
365        let order_by = select.order_by.clone();
366        let limit = select.limit;
367        let offset = select.offset;
368        let first = select.first;
369
370        let id = self.sources.len();
371        // The store's FROM-term number is reserved before anything is bound, so
372        // that a self-reference inside the step arm can name the store it will
373        // read without the two being bound in an impossible order.
374        self.sources.push(BoundSource {
375            index_hint: crate::bind::IndexChoice::Any,
376            id,
377            rows: SourceRows::Table,
378            table: std::rc::Rc::new(TableInfo::subquery(alias.clone(), 0, Vec::new())),
379            alias: alias.clone(),
380            join,
381            constraint: None,
382            suppressed: Vec::new(),
383            index_exprs: Vec::new(),
384            written_schema: None,
385            derived: Default::default(),
386        });
387
388        let seed = self.bind_isolated_arm(first)?;
389        let table = subquery_table(&alias, &cte.columns, &seed);
390        named_columns_fit(&alias, &cte.columns, seed.columns.len(), span)?;
391        self.recursing.push(RecursiveTarget {
392            folded: cte.folded.clone(),
393            id,
394            table: table.clone(),
395            referenced: false,
396        });
397        let mut seeds = vec![(CompoundOp::UnionAll, seed)];
398        let mut steps = Vec::new();
399        let mut outcome = Ok(());
400        for (op, arm) in &arms {
401            if !matches!(op, CompoundOp::Union | CompoundOp::UnionAll) {
402                outcome = Err(ParseError::new(
403                    ParseErrorKind::Unsupported("recursive query does not use UNION or UNION ALL"),
404                    span,
405                ));
406                break;
407            }
408            if let Some(target) = self.recursing.last_mut() {
409                target.referenced = false;
410            }
411            let bound = match self.bind_isolated_arm(*arm) {
412                Ok(bound) => bound,
413                Err(reason) => {
414                    outcome = Err(reason);
415                    break;
416                }
417            };
418            let referenced = self
419                .recursing
420                .last()
421                .is_some_and(|target| target.referenced);
422            if let Err(refusal) =
423                arm_refusal(&bound, table.columns.len(), referenced, *op, &alias, span)
424            {
425                outcome = Err(refusal);
426                break;
427            }
428            if referenced {
429                steps.push((*op, bound));
430            } else {
431                seeds.push((*op, bound));
432            }
433        }
434        self.recursing.pop();
435        outcome?;
436        // The ORDER BY, LIMIT and OFFSET belong to the whole recursive query:
437        // SQLite orders its queue by them and stops the recursion at the limit,
438        // so they are kept on the body rather than on the seed arm.
439        let seed_columns = seeds
440            .first()
441            .map_or_else(Vec::new, |(_, seed)| seed.columns.clone());
442        // An ORDER BY name may come from any arm, as in every other compound.
443        let other_arms: Vec<(ast::CompoundOp, crate::bind::BoundSelect)> =
444            seeds.iter().skip(1).chain(steps.iter()).cloned().collect();
445        let order_by = self.bind_compound_order_by(&order_by, &seed_columns, &other_arms)?;
446        let limit = limit.map(|expr| self.bind_expr(expr)).transpose()?;
447        let offset = offset.map(|expr| self.bind_expr(expr)).transpose()?;
448        let mut source = BoundSource {
449            index_hint: crate::bind::IndexChoice::Any,
450            id,
451            rows: SourceRows::Recursive(Box::new(RecursiveBody {
452                seeds,
453                steps,
454                order_by,
455                limit,
456                offset,
457            })),
458            table: std::rc::Rc::new(table),
459            alias,
460            join,
461            constraint: None,
462            suppressed: Vec::new(),
463            index_exprs: Vec::new(),
464            written_schema: None,
465            derived: Default::default(),
466        };
467        if let SourceRows::Recursive(body) = &mut source.rows {
468            if body.steps.is_empty() {
469                // Declared recursive, never refers to itself: an ordinary
470                // compound wearing the keyword.
471                let mut arms = core::mem::take(&mut body.seeds);
472                if arms.is_empty() {
473                    return Err(unsupported("missing select core", span));
474                }
475                let mut head = arms.remove(0).1;
476                head.compounds = arms;
477                head.order_by = core::mem::take(&mut body.order_by);
478                head.limit = body.limit.take();
479                head.offset = body.offset.take();
480                source.rows = SourceRows::Subquery(Box::new(head));
481            }
482        }
483        if let Some(slot) = self.sources.get_mut(id) {
484            *slot = source;
485        }
486        if let Some(scope) = self.scopes.last_mut() {
487            scope.push(id);
488        }
489        Ok(())
490    }
491}
492
493impl<'a> Binder<'a> {
494    /// Binds a FROM term that names a common table expression.
495    ///
496    /// @param cte - the expression the name stands for
497    /// @param folded - the folded name, to tell a recursive reference from a plain one
498    /// @param alias - the alias written on the term, if any
499    /// @param join - how the term joins the ones before it
500    /// @param span - where the term is, for an error
501    pub(super) fn bind_cte_term(
502        &mut self,
503        cte: CteBinding,
504        folded: &[u8],
505        alias: Option<ast::NameId>,
506        join: JoinKind,
507        span: Span,
508    ) -> Result<(), ParseError> {
509        let alias = match alias {
510            Some(alias) => self.ast.text(alias).to_vec(),
511            None => cte.name.clone(),
512        };
513        // A definition already being bound cannot be bound again: that
514        // is a cycle, and following it does not end.
515        if self.binding_ctes.contains(&cte.select) {
516            // SQLite names the expression and points at nothing.
517            let _ = span;
518            return Err(ParseError::new(
519                ParseErrorKind::Refused(format!(
520                    "circular reference: {}",
521                    String::from_utf8_lossy(&cte.name)
522                )),
523                Span::default(),
524            ));
525        }
526        self.binding_ctes.push(cte.select);
527        // **A definition sees the names of its own `WITH` and the ones
528        // outside it, not the ones of a `WITH` nested in the query that
529        // happens to use it.** The levels above the definition's own are set
530        // aside while its body is bound and put back afterwards.
531        let hidden = self
532            .ctes
533            .split_off(cte.level.saturating_add(1).min(self.ctes.len()));
534        // **`RECURSIVE` is a keyword SQLite does not require.** A CTE
535        // whose FROM names itself *is* the recursion, written or not,
536        // and reading the keyword as the only evidence sent this
537        // binder round the same definition until the stack ran out.
538        let outcome = if cte.recursive || self.select_names_itself(cte.select, folded) {
539            self.bind_recursive_cte(&cte, alias, join, span)
540        } else {
541            let bound =
542                self.bind_subquery_term(cte.select, Some(alias), cte.columns.clone(), join, span);
543            if bound.is_ok() {
544                self.share_last_source(&cte);
545            }
546            bound
547        };
548        self.ctes.extend(hidden);
549        self.binding_ctes.pop();
550        // A recursive CTE's references to itself, inside its own arms, are the
551        // recursion and not uses of it.
552        let own = match self.ast.select(cte.select) {
553            Some(query) => core::iter::once(query.first)
554                .chain(query.compounds.iter().map(|(_, arm)| *arm))
555                .filter(|arm| self.core_names_cte(*arm, &cte.folded))
556                .count() as u32,
557            None => 0,
558        };
559        let uses = self.name_uses(&cte.folded).saturating_sub(own);
560        // The reference is the term this binding just added to the block's
561        // scope; the sources the body bound come before it.
562        let id = self.scope().last().copied();
563        if let Some(source) = id.and_then(|id| self.sources.get_mut(id)) {
564            source.derived = super::DerivedNote {
565                cte: true,
566                materialized: cte.materialized,
567                uses,
568                name: cte.name.clone(),
569                ..super::DerivedNote::default()
570            };
571        }
572        outcome
573    }
574
575    /// Finds the table a FROM term names, falling back to a table valued
576    /// function when the view's own database does not hold the name.
577    ///
578    /// A name that is not a table of the view's database may still be a table
579    /// valued function such as `json_each`, which belongs to no schema.
580    ///
581    /// @param database - the schema written on the term, if any
582    /// @param database_name - the schema to look in, folded
583    /// @param folded - the table name, folded
584    pub(super) fn find_term_table(
585        &self,
586        database: Option<ast::NameId>,
587        database_name: Option<Vec<u8>>,
588        folded: &[u8],
589    ) -> (Option<&'a TableInfo>, Option<Vec<u8>>) {
590        let found = self.catalog.find_table(database_name.as_deref(), folded);
591        if found.is_none() && database.is_none() && database_name.is_some() {
592            let eponymous = self
593                .catalog
594                .find_table(None, folded)
595                .filter(|table| table.kind == crate::catalog_view::TableKind::Virtual);
596            if eponymous.is_some() {
597                return (eponymous, None);
598            }
599        }
600        (found, database_name)
601    }
602}
603
604/// Refuses a recursive common table expression that names a different number
605/// of columns than its first arm makes.
606///
607/// @param alias - the expression's name, for the message
608/// @param named - the column names written after the name, if any
609/// @param width - the number of columns of the first arm
610/// @param span - where the statement is, for the error
611fn named_columns_fit<T>(
612    alias: &[u8],
613    named: &[T],
614    width: usize,
615    span: Span,
616) -> Result<(), ParseError> {
617    if named.is_empty() || named.len() == width {
618        return Ok(());
619    }
620    Err(super::refusal::named_column_count(
621        alias,
622        width,
623        named.len(),
624        span,
625    ))
626}
627
628/// Refuses an arm of a recursive common table expression that SQLite refuses.
629///
630/// An arm of another width than the first is refused, as in any compound. A
631/// recursive step of the wrong width was run, and `WITH i(x) AS (SELECT 1
632/// UNION ALL SELECT x+1, x*2 FROM i)` never finished. A recursive step is also
633/// held to [`recursive_step_refusal`].
634///
635/// @param arm - the bound arm
636/// @param width - the number of columns of the first arm
637/// @param recursive - whether the arm reads the recursive table
638/// @param op - the compound operator in front of the arm
639/// @param alias - the recursive table's name, for the message
640/// @param span - where the statement is, for the error
641fn arm_refusal(
642    arm: &crate::bind::BoundSelect,
643    width: usize,
644    recursive: bool,
645    op: CompoundOp,
646    alias: &[u8],
647    span: Span,
648) -> Result<(), ParseError> {
649    if arm.columns.len() != width {
650        return Err(super::refusal::compound_width_mismatch(op, span));
651    }
652    match recursive
653        .then(|| recursive_step_refusal(arm, alias))
654        .flatten()
655    {
656        Some(reason) => Err(super::refused(reason, span)),
657        None => Ok(()),
658    }
659}
660
661/// Returns why SQLite refuses a recursive step, or `None` when it does not.
662///
663/// **An aggregate in the step never finishes.** Each pass of the recursion
664/// feeds the rows the last pass produced back in, and `SELECT count(*) FROM r`
665/// produces a row from no rows, so the queue never empties: `WITH RECURSIVE
666/// r(n) AS (SELECT 1 UNION ALL SELECT count(*) FROM r) SELECT * FROM r` ran
667/// until it was stopped. SQLite refuses an aggregate, a `GROUP BY` and a
668/// window function in a recursive step, and a step that names the recursive
669/// table twice, before it runs anything.
670///
671/// @param step - the bound recursive arm
672/// @param alias - the recursive table's name, for the message
673fn recursive_step_refusal(step: &crate::bind::BoundSelect, alias: &[u8]) -> Option<String> {
674    if !step.aggregates.is_empty() || !step.group_by.is_empty() {
675        return Some("recursive aggregate queries not supported".to_string());
676    }
677    if !step.windows.is_empty() {
678        return Some("cannot use window functions in recursive queries".to_string());
679    }
680    let references = step
681        .sources
682        .iter()
683        .filter(|source| matches!(source.rows, SourceRows::RecursiveSelf { .. }))
684        .count();
685    (references > 1).then(|| {
686        format!(
687            "multiple references to recursive table: {}",
688            String::from_utf8_lossy(alias)
689        )
690    })
691}