Skip to main content

unisql_sqlparser_adapter/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! sqlparser adapter: text -> IR for the dialects that sqlparser can parse. The per-dialect
3//! differences are the [`Rules`]; no parser type escapes this crate. **Anything the IR cannot represent is `ReadError::NotInIr`, never a pass-through.**
4//!
5//! `Query`, `Select`, `Insert`, `Update` and `Delete` are destructured with every field named and no
6//! `..`: a sqlparser upgrade that adds a field fails the build here, forcing a decision rather than
7//! silently dropping a construct.
8
9use std::cell::RefCell;
10
11use sqlparser::ast as sp;
12use sqlparser::dialect::Dialect;
13use sqlparser::parser::Parser;
14use unisql_core::ast::*;
15use unisql_core::{Allowance, Column, ColumnClass, Read, ReadError, ReadNote, Schema};
16
17/// What differs between source dialects at the AST-to-IR step.
18pub struct Rules {
19    pub name: &'static str,
20    pub dialect: fn() -> Box<dyn Dialect>,
21    /// Quoted identifiers are case-insensitive too (SQLite): fold everything to lower case.
22    pub fold_quoted: bool,
23    /// `NULL` sorts as the smallest value (SQLite) rather than the largest (PostgreSQL).
24    pub nulls_smallest: bool,
25    /// Parameters: `$n` (PostgreSQL) or `?`, `?n`, `:name`, `@name`, `$name` (SQLite).
26    pub question_placeholders: bool,
27    /// `LIKE` is case-insensitive by default (SQLite, ASCII only).
28    pub like_ci_default: bool,
29    /// `LIKE` has a default escape character (PostgreSQL: backslash); otherwise none.
30    pub like_default_escape: bool,
31    /// A negative `LIMIT` means no limit (SQLite).
32    pub negative_limit_unlimited: bool,
33}
34
35pub static POSTGRES: Rules = Rules {
36    name: "postgres",
37    dialect: || Box::new(sqlparser::dialect::PostgreSqlDialect {}),
38    fold_quoted: false,
39    nulls_smallest: false,
40    question_placeholders: false,
41    like_ci_default: false,
42    like_default_escape: true,
43    negative_limit_unlimited: false,
44};
45
46pub static MYSQL: Rules = Rules {
47    name: "mysql",
48    dialect: || Box::new(sqlparser::dialect::MySqlDialect {}),
49    fold_quoted: true,
50    nulls_smallest: true,
51    question_placeholders: true,
52    like_ci_default: false,
53    like_default_escape: true,
54    negative_limit_unlimited: false,
55};
56
57pub static SQLITE: Rules = Rules {
58    name: "sqlite",
59    dialect: || Box::new(sqlparser::dialect::SQLiteDialect {}),
60    fold_quoted: true,
61    nulls_smallest: true,
62    question_placeholders: true,
63    like_ci_default: true,
64    like_default_escape: false,
65    negative_limit_unlimited: true,
66};
67
68struct Cx {
69    rules: &'static Rules,
70    max_param: u32,
71    names: Vec<(String, u32)>,
72    notes: Vec<ReadNote>,
73}
74
75thread_local! {
76    static CX: RefCell<Option<Cx>> = const { RefCell::new(None) };
77}
78
79fn rules() -> &'static Rules {
80    CX.with(|c| c.borrow().as_ref().expect("reader context").rules)
81}
82
83fn add_note(construct: &str, note: &str, allowance: Allowance) {
84    CX.with(|c| {
85        let mut b = c.borrow_mut();
86        let cx = b.as_mut().expect("reader context");
87        let n = ReadNote {
88            construct: construct.into(),
89            note: note.into(),
90            allowance,
91        };
92        if !cx.notes.contains(&n) {
93            cx.notes.push(n);
94        }
95    });
96}
97
98fn bump_param(explicit: Option<u32>, name: Option<&str>) -> u32 {
99    CX.with(|c| {
100        let mut b = c.borrow_mut();
101        let cx = b.as_mut().expect("reader context");
102        match (explicit, name) {
103            (Some(n), _) => {
104                cx.max_param = cx.max_param.max(n);
105                n
106            }
107            (None, Some(nm)) => {
108                if let Some((_, n)) = cx.names.iter().find(|(k, _)| k == nm) {
109                    return *n;
110                }
111                cx.max_param += 1;
112                let n = cx.max_param;
113                cx.names.push((nm.to_string(), n));
114                n
115            }
116            (None, None) => {
117                cx.max_param += 1;
118                cx.max_param
119            }
120        }
121    })
122}
123
124type R<T> = Result<T, ReadError>;
125
126fn no<T>(construct: &str, detail: &str) -> R<T> {
127    Err(ReadError::NotInIr {
128        construct: construct.to_string(),
129        detail: detail.to_string(),
130    })
131}
132
133/// Read exactly one statement written in the dialect described by `rules`.
134pub fn read(sql: &str, rules: &'static Rules) -> R<Read> {
135    CX.with(|c| {
136        *c.borrow_mut() = Some(Cx {
137            rules,
138            max_param: 0,
139            names: vec![],
140            notes: vec![],
141        })
142    });
143    let out = (|| {
144        let d = (rules.dialect)();
145        // MySQL's default sql_mode makes `||` a logical OR with OR's precedence, but the parser
146        // gives it concatenation's. Rewrite the operator token in the text (by position, so nothing
147        // else is re-spelled) before parsing, and say so.
148        let rewritten;
149        let sql = if rules.name == "mysql" {
150            match pipes_to_or(sql, d.as_ref()) {
151                Some(s) => {
152                    add_note(
153                        "pipes_as_or",
154                        "MySQL `||` is logical OR in the default sql_mode; under PIPES_AS_CONCAT it is concatenation",
155                        Allowance::PipesAsLogicalOr,
156                    );
157                    rewritten = s;
158                    rewritten.as_str()
159                }
160                None => sql,
161            }
162        } else {
163            sql
164        };
165        let mut stmts =
166            Parser::parse_sql(d.as_ref(), sql).map_err(|e| ReadError::Syntax(e.to_string()))?;
167        if stmts.len() != 1 {
168            return no(
169                "multiple_statements",
170                &format!("expected one statement, found {}", stmts.len()),
171            );
172        }
173        statement(stmts.remove(0))
174    })();
175    let cx = CX.with(|c| c.borrow_mut().take()).expect("reader context");
176    out.map(|stmt| Read {
177        stmt,
178        notes: cx.notes,
179    })
180}
181
182/// `sql` with each `||` operator token replaced by ` OR `; `None` when there is none (or the text does
183/// not tokenize, which the parser then reports).
184fn pipes_to_or(sql: &str, dialect: &dyn sqlparser::dialect::Dialect) -> Option<String> {
185    use sqlparser::tokenizer::{Token, Tokenizer};
186    let tokens = Tokenizer::new(dialect, sql).tokenize_with_location().ok()?;
187    let line_starts: Vec<usize> = std::iter::once(0)
188        .chain(
189            sql.chars()
190                .enumerate()
191                .filter(|(_, c)| *c == '\n')
192                .map(|(i, _)| i + 1),
193        )
194        .collect();
195    let mut spots: Vec<usize> = tokens
196        .iter()
197        .filter(|t| t.token == Token::StringConcat)
198        .filter_map(|t| {
199            let line = usize::try_from(t.span.start.line).ok()?.checked_sub(1)?;
200            let col = usize::try_from(t.span.start.column).ok()?.checked_sub(1)?;
201            Some(line_starts.get(line)? + col)
202        })
203        .collect();
204    if spots.is_empty() {
205        return None;
206    }
207    spots.sort_unstable();
208    let chars: Vec<char> = sql.chars().collect();
209    let mut out = String::new();
210    let mut at = 0;
211    for s in spots {
212        if s < at || s + 2 > chars.len() || chars[s] != '|' || chars[s + 1] != '|' {
213            return None;
214        }
215        out.extend(&chars[at..s]);
216        out.push_str(" OR ");
217        at = s + 2;
218    }
219    out.extend(&chars[at..]);
220    Some(out)
221}
222
223/// The classes of the base-table columns a DDL script declares, read for types only: this is not DDL
224/// translation, and no statement of it is ever written out.
225#[derive(Clone, Debug, Default)]
226pub struct SchemaRead {
227    pub schema: Schema,
228    /// Statements the parser could not read (a function body, a `DO` block): each head and reason.
229    /// They are skipped, which can only leave a column unregistered (unknown).
230    pub skipped: Vec<String>,
231}
232
233/// Class of a declared type, from its spelling (`None`: a type UniSQL does not classify). MySQL's
234/// `tinyint(1)` is numeric; `boolean` is `Boolean` only because the declared type says so.
235fn class_of_type(dt: &sp::DataType) -> Option<ColumnClass> {
236    let t = dt.to_string().to_ascii_lowercase();
237    if t.ends_with("[]") || t.starts_with("array") {
238        return None;
239    }
240    let base: String = t
241        .chars()
242        .take_while(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == ' ')
243        .collect();
244    let base = base.trim();
245    let first = base.split_whitespace().next().unwrap_or("");
246    Some(match base {
247        "character varying" | "character" | "national character varying" | "national character" => {
248            ColumnClass::Text
249        }
250        "double precision" => ColumnClass::Numeric,
251        b if b.starts_with("timestamp")
252            || b == "time"
253            || b.starts_with("time with")
254            || b.starts_with("time without") =>
255        {
256            ColumnClass::Other
257        }
258        _ => match first {
259            "text" | "varchar" | "char" | "nchar" | "nvarchar" | "tinytext" | "mediumtext"
260            | "longtext" | "string" | "bpchar" | "clob" => ColumnClass::Text,
261            "int" | "integer" | "smallint" | "bigint" | "tinyint" | "mediumint" | "int2"
262            | "int4" | "int8" | "serial" | "bigserial" | "smallserial" | "decimal" | "numeric"
263            | "dec" | "float" | "float4" | "float8" | "real" | "double" | "number" => {
264                ColumnClass::Numeric
265            }
266            "boolean" | "bool" => ColumnClass::Boolean,
267            "date" | "datetime" | "timestamptz" | "year" | "interval" | "uuid" | "json"
268            | "jsonb" | "bytea" | "blob" | "tinyblob" | "mediumblob" | "longblob" | "binary"
269            | "varbinary" | "bit" | "inet" | "cidr" | "macaddr" => ColumnClass::Other,
270            _ => return None,
271        },
272    })
273}
274
275fn column_from_def(c: &sp::ColumnDef) -> Column {
276    let class = class_of_type(&c.data_type);
277    let collation = c.options.iter().find_map(|o| match &o.option {
278        sp::ColumnOption::Collation(n) => {
279            Some(n.to_string().trim_matches(['"', '`', '\'']).to_string())
280        }
281        _ => None,
282    });
283    let mut col = Column::new(class.unwrap_or(ColumnClass::Unknown));
284    col.collation = collation;
285    col
286}
287
288/// Read `CREATE TABLE`, `ALTER TABLE .. ADD|DROP|ALTER COLUMN` and `DROP TABLE` out of a DDL script
289/// (a migration file) in the dialect described by `rules`. Statements are split on `;` tokens, so a
290/// dollar-quoted body does not split; a statement that does not parse is listed in `skipped`.
291pub fn read_schema(ddl: &str, rules: &'static Rules) -> R<SchemaRead> {
292    use sqlparser::tokenizer::{Token, Tokenizer};
293    let d = (rules.dialect)();
294    let tokens = Tokenizer::new(d.as_ref(), ddl)
295        .tokenize_with_location()
296        .map_err(|e| ReadError::Syntax(e.to_string()))?;
297    let chars: Vec<char> = ddl.chars().collect();
298    let line_starts: Vec<usize> = std::iter::once(0)
299        .chain(
300            chars
301                .iter()
302                .enumerate()
303                .filter(|(_, c)| **c == '\n')
304                .map(|(i, _)| i + 1),
305        )
306        .collect();
307    let offset = |loc: sqlparser::tokenizer::Location| -> Option<usize> {
308        let line = usize::try_from(loc.line).ok()?.checked_sub(1)?;
309        let col = usize::try_from(loc.column).ok()?.checked_sub(1)?;
310        Some(line_starts.get(line)? + col)
311    };
312    let mut cuts: Vec<usize> = tokens
313        .iter()
314        .filter(|t| t.token == Token::SemiColon)
315        .filter_map(|t| offset(t.span.start))
316        .collect();
317    cuts.push(chars.len());
318    let mut out = SchemaRead::default();
319    let mut from = 0;
320    for cut in cuts {
321        if cut < from || cut > chars.len() {
322            continue;
323        }
324        let text: String = chars[from..cut].iter().collect();
325        from = (cut + 1).min(chars.len());
326        if text.trim().is_empty() {
327            continue;
328        }
329        let stmts = match Parser::parse_sql(d.as_ref(), &text) {
330            Ok(s) => s,
331            Err(e) => {
332                let head: String = text
333                    .split_whitespace()
334                    .take(4)
335                    .collect::<Vec<_>>()
336                    .join(" ");
337                out.skipped.push(format!("{head}: {e}"));
338                continue;
339            }
340        };
341        for st in stmts {
342            apply_ddl(&mut out.schema, st);
343        }
344    }
345    Ok(out)
346}
347
348fn apply_ddl(schema: &mut Schema, st: sp::Statement) {
349    match st {
350        sp::Statement::CreateTable(ct) => {
351            if ct.query.is_some() || ct.like.is_some() {
352                return;
353            }
354            let Ok(name) = object_name(&ct.name) else {
355                return;
356            };
357            schema.drop_table(&name);
358            for c in &ct.columns {
359                schema.set_column(name.0.clone(), ident(&c.name), column_from_def(c));
360            }
361        }
362        sp::Statement::AlterTable(at) => {
363            let Ok(name) = object_name(&at.name) else {
364                return;
365            };
366            for op in at.operations {
367                match op {
368                    sp::AlterTableOperation::AddColumn { column_def, .. } => {
369                        schema.set_column(
370                            name.0.clone(),
371                            ident(&column_def.name),
372                            column_from_def(&column_def),
373                        );
374                    }
375                    sp::AlterTableOperation::DropColumn { column_names, .. } => {
376                        for c in &column_names {
377                            schema.drop_column(&name, &ident(c));
378                        }
379                    }
380                    sp::AlterTableOperation::AlterColumn {
381                        column_name,
382                        op: sp::AlterColumnOperation::SetDataType { data_type, .. },
383                    } => {
384                        let class = class_of_type(&data_type).unwrap_or(ColumnClass::Unknown);
385                        // A new type resets the collation unless the statement states one.
386                        schema.set_column(name.0.clone(), ident(&column_name), Column::new(class));
387                    }
388                    _ => {}
389                }
390            }
391        }
392        sp::Statement::Drop {
393            object_type: sp::ObjectType::Table,
394            names,
395            ..
396        } => {
397            for n in &names {
398                if let Ok(name) = object_name(n) {
399                    schema.drop_table(&name);
400                }
401            }
402        }
403        _ => {}
404    }
405}
406
407fn statement(s: sp::Statement) -> R<Statement> {
408    match s {
409        sp::Statement::Query(q)
410            if matches!(
411                &*q.body,
412                sp::SetExpr::Insert(_) | sp::SetExpr::Update(_) | sp::SetExpr::Delete(_)
413            ) =>
414        {
415            // `WITH .. INSERT|UPDATE|DELETE ..` is a query node in the parser's tree.
416            let mut q = *q;
417            let with = q.with.take();
418            if q.to_string() != q.body.to_string() {
419                return no(
420                    "query_clause",
421                    "clauses around a WITH .. INSERT/UPDATE/DELETE",
422                );
423            }
424            let inner = match *q.body {
425                sp::SetExpr::Insert(st) | sp::SetExpr::Update(st) | sp::SetExpr::Delete(st) => {
426                    statement(st)?
427                }
428                _ => unreachable!(),
429            };
430            match with {
431                None => Ok(inner),
432                Some(w) => {
433                    let (ctes, recursive) = read_ctes(w)?;
434                    Ok(Statement::With(WithStatement {
435                        recursive,
436                        ctes,
437                        stmt: Box::new(inner),
438                    }))
439                }
440            }
441        }
442        sp::Statement::Query(q) => Ok(Statement::Query(Box::new(query(*q)?))),
443        sp::Statement::Insert(i) => insert(i),
444        sp::Statement::Update(u) => update(u),
445        sp::Statement::Delete(d) => delete(d),
446        other => no(
447            "statement",
448            &format!(
449                "{} is not a DML statement the first slice reads",
450                head(&other)
451            ),
452        ),
453    }
454}
455
456fn head(x: &impl std::fmt::Debug) -> String {
457    let s = format!("{x:?}");
458    s.split(|c: char| !c.is_alphanumeric() && c != '_')
459        .next()
460        .unwrap_or("")
461        .to_string()
462}
463
464/// Identifier folding: unquoted identifiers fold to lower case; quoted ones are exact, except where
465/// the dialect treats quoted names case-insensitively too (SQLite).
466fn ident(i: &sp::Ident) -> String {
467    if i.quote_style.is_some() && !rules().fold_quoted {
468        i.value.clone()
469    } else {
470        i.value.to_lowercase()
471    }
472}
473
474fn object_name(n: &sp::ObjectName) -> R<Name> {
475    let mut parts = Vec::new();
476    for p in &n.0 {
477        match p {
478            sp::ObjectNamePart::Identifier(i) => parts.push(ident(i)),
479            other => return no("object_name_part", &head(other)),
480        }
481    }
482    Ok(Name(parts))
483}
484
485fn single_name(n: &sp::ObjectName) -> R<String> {
486    let nm = object_name(n)?;
487    if nm.0.len() != 1 {
488        return no(
489            "qualified_column_target",
490            "expected an unqualified column name",
491        );
492    }
493    Ok(nm.0.into_iter().next().unwrap())
494}
495
496/// The `WITH` list of a query or of a data-modifying statement.
497fn read_ctes(w: sp::With) -> R<(Vec<Cte>, bool)> {
498    let sp::With {
499        with_token: _,
500        recursive,
501        cte_tables,
502    } = w;
503    let mut ctes = Vec::new();
504    for c in cte_tables {
505        let sp::Cte {
506            alias,
507            query: cq,
508            from,
509            materialized,
510            closing_paren_token: _,
511        } = c;
512        if from.is_some() || materialized.is_some() {
513            return no("cte_option", "MATERIALIZED / FROM in a CTE");
514        }
515        let mut columns = Vec::new();
516        for col in &alias.columns {
517            if col.data_type.is_some() {
518                return no("cte_column_type", "typed CTE column");
519            }
520            columns.push(ident(&col.name));
521        }
522        let dml = matches!(
523            &*cq.body,
524            sp::SetExpr::Insert(_) | sp::SetExpr::Update(_) | sp::SetExpr::Delete(_)
525        );
526        let cte_body = if dml {
527            // Nothing but the statement itself: a WITH / ORDER BY / LIMIT around it would be
528            // dropped silently otherwise.
529            if cq.to_string() != cq.body.to_string() {
530                return no("cte_dml_clause", "clauses around a data-modifying CTE");
531            }
532            match *cq.body {
533                sp::SetExpr::Insert(st) | sp::SetExpr::Update(st) | sp::SetExpr::Delete(st) => {
534                    CteBody::Dml(Box::new(statement(st)?))
535                }
536                _ => unreachable!(),
537            }
538        } else {
539            CteBody::Query(Box::new(query(*cq)?))
540        };
541        ctes.push(Cte {
542            name: ident(&alias.name),
543            columns,
544            body: cte_body,
545        });
546    }
547    Ok((ctes, recursive))
548}
549
550fn query(q: sp::Query) -> R<Query> {
551    let sp::Query {
552        with,
553        body,
554        order_by,
555        limit_clause,
556        fetch,
557        locks,
558        for_clause,
559        settings,
560        format_clause,
561        pipe_operators,
562    } = q;
563    if fetch.is_some() {
564        return no("fetch_first", "FETCH FIRST is not in the IR (use LIMIT)");
565    }
566    if for_clause.is_some()
567        || settings.is_some()
568        || format_clause.is_some()
569        || !pipe_operators.is_empty()
570    {
571        return no("query_clause", "FOR/SETTINGS/FORMAT/pipe operators");
572    }
573    let mut ctes = Vec::new();
574    let mut recursive = false;
575    if let Some(w) = with {
576        (ctes, recursive) = read_ctes(w)?;
577    }
578    let mut body = set_expr(*body)?;
579    let mut lock = None;
580    match locks.len() {
581        0 => {}
582        1 => {
583            let sp::LockClause {
584                lock_type,
585                of,
586                nonblock,
587            } = locks.into_iter().next().unwrap();
588            lock = Some(RowLockClause {
589                strength: match lock_type {
590                    sp::LockType::Update => RowLock::Update,
591                    sp::LockType::Share => RowLock::Share,
592                },
593                of: match of {
594                    Some(n) => Some(object_name(&n)?),
595                    None => None,
596                },
597                wait: match nonblock {
598                    None => LockWait::Block,
599                    Some(sp::NonBlock::Nowait) => LockWait::NoWait,
600                    Some(sp::NonBlock::SkipLocked) => LockWait::SkipLocked,
601                },
602            });
603        }
604        _ => return no("row_lock_multiple", "several locking clauses"),
605    }
606    if let Some(l) = lock {
607        match &mut body {
608            SetExpr::Select(s) => s.lock = Some(l),
609            _ => return no("row_lock_on_set_op", "locking clause on a set operation"),
610        }
611    }
612    let mut order = Vec::new();
613    if let Some(ob) = order_by {
614        let sp::OrderBy { kind, interpolate } = ob;
615        if interpolate.is_some() {
616            return no("order_by_interpolate", "INTERPOLATE");
617        }
618        match kind {
619            sp::OrderByKind::Expressions(items) => {
620                for it in items {
621                    order.push(order_item(it)?);
622                }
623            }
624            sp::OrderByKind::All(_) => return no("order_by_all", "ORDER BY ALL"),
625        }
626    }
627    let (mut limit, mut offset) = (None, None);
628    match limit_clause {
629        None => {}
630        Some(sp::LimitClause::LimitOffset {
631            limit: l,
632            offset: o,
633            limit_by,
634        }) => {
635            if !limit_by.is_empty() {
636                return no("limit_by", "LIMIT .. BY");
637            }
638            limit = l.map(expr).transpose()?;
639            if rules().negative_limit_unlimited {
640                // SQLite: a negative LIMIT means "no limit".
641                let neg = matches!(&limit, Some(Expr::Unary { op: UnaryOp::Minus, expr })
642                    if matches!(**expr, Expr::Literal(Literal::Number(_))));
643                if neg {
644                    limit = None;
645                }
646            }
647            if let Some(o) = o {
648                let sp::Offset { value, rows } = o;
649                if !matches!(rows, sp::OffsetRows::None) {
650                    return no("offset_rows", "OFFSET .. ROWS");
651                }
652                offset = Some(expr(value)?);
653            }
654        }
655        Some(sp::LimitClause::OffsetCommaLimit {
656            offset: o,
657            limit: l,
658        }) => {
659            offset = Some(expr(o)?);
660            limit = Some(expr(l)?);
661        }
662    }
663    Ok(Query {
664        with: ctes,
665        recursive,
666        body,
667        order_by: order,
668        limit,
669        offset,
670    })
671}
672
673fn order_item(o: sp::OrderByExpr) -> R<OrderItem> {
674    let sp::OrderByExpr {
675        expr: e,
676        options,
677        with_fill,
678    } = o;
679    if with_fill.is_some() {
680        return no("order_by_with_fill", "WITH FILL");
681    }
682    let desc = matches!(options.sort, Some(sp::OrderBySort::Desc));
683    // The source's default NULL placement, made explicit so any target preserves it: NULL is the
684    // largest value on PostgreSQL (first when descending) and the smallest on SQLite (first ascending).
685    let default_first = if rules().nulls_smallest { !desc } else { desc };
686    let nulls_first = options.nulls_first.unwrap_or(default_first);
687    Ok(OrderItem {
688        expr: expr(e)?,
689        desc,
690        nulls_first: Some(nulls_first),
691    })
692}
693
694fn set_expr(s: sp::SetExpr) -> R<SetExpr> {
695    match s {
696        sp::SetExpr::Select(sel) => Ok(SetExpr::Select(Box::new(select(*sel)?))),
697        sp::SetExpr::SetOperation {
698            left,
699            op,
700            set_quantifier,
701            right,
702        } => {
703            let op = match op {
704                sp::SetOperator::Union => SetOp::Union,
705                sp::SetOperator::Intersect => SetOp::Intersect,
706                sp::SetOperator::Except => SetOp::Except,
707                sp::SetOperator::Minus => return no("minus", "MINUS is not PostgreSQL"),
708            };
709            let all = match set_quantifier {
710                sp::SetQuantifier::All => true,
711                sp::SetQuantifier::Distinct | sp::SetQuantifier::None => false,
712                other => return no("set_quantifier", &head(&other)),
713            };
714            Ok(SetExpr::SetOp {
715                op,
716                all,
717                left: Box::new(set_expr(*left)?),
718                right: Box::new(set_expr(*right)?),
719            })
720        }
721        sp::SetExpr::Query(q) => {
722            let q = query(*q)?;
723            if !q.with.is_empty()
724                || !q.order_by.is_empty()
725                || q.limit.is_some()
726                || q.offset.is_some()
727            {
728                return no(
729                    "parenthesised_query",
730                    "parenthesised query with WITH/ORDER/LIMIT as a set operand",
731                );
732            }
733            Ok(q.body)
734        }
735        sp::SetExpr::Values(_) => no("values_query", "a bare VALUES query outside INSERT"),
736        other => no("set_expr", &head(&other)),
737    }
738}
739
740fn select(s: sp::Select) -> R<Select> {
741    let sp::Select {
742        select_token: _,
743        optimizer_hints,
744        distinct,
745        select_modifiers,
746        top,
747        top_before_distinct: _,
748        projection,
749        exclude,
750        into,
751        from,
752        lateral_views,
753        prewhere,
754        selection,
755        connect_by,
756        group_by,
757        cluster_by,
758        distribute_by,
759        sort_by,
760        having,
761        named_window,
762        qualify,
763        window_before_qualify: _,
764        value_table_mode,
765        flavor: _,
766    } = s;
767    if !optimizer_hints.is_empty()
768        || select_modifiers.is_some()
769        || top.is_some()
770        || exclude.is_some()
771        || into.is_some()
772        || !lateral_views.is_empty()
773        || prewhere.is_some()
774        || !connect_by.is_empty()
775        || !cluster_by.is_empty()
776        || !distribute_by.is_empty()
777        || !sort_by.is_empty()
778        || !named_window.is_empty()
779        || qualify.is_some()
780        || value_table_mode.is_some()
781    {
782        return no(
783            "select_clause",
784            "a SELECT clause the IR does not represent (TOP/INTO/WINDOW/QUALIFY/hints/...)",
785        );
786    }
787    let distinct = match distinct {
788        None | Some(sp::Distinct::All) => Distinct::No,
789        Some(sp::Distinct::Distinct) => Distinct::Yes,
790        Some(sp::Distinct::On(es)) => Distinct::On(es.into_iter().map(expr).collect::<R<_>>()?),
791    };
792    let group_by = match group_by {
793        sp::GroupByExpr::Expressions(es, mods) => {
794            if !mods.is_empty() {
795                return no("group_by_modifier", "ROLLUP/CUBE/TOTALS modifiers");
796            }
797            es.into_iter().map(expr).collect::<R<Vec<_>>>()?
798        }
799        sp::GroupByExpr::All(_) => return no("group_by_all", "GROUP BY ALL"),
800    };
801    let mut proj = Vec::new();
802    for it in projection {
803        proj.push(select_item(it)?);
804    }
805    let mut from_refs = Vec::new();
806    for t in from {
807        from_refs.push(table_with_joins(t)?);
808    }
809    Ok(Select {
810        distinct,
811        projection: proj,
812        from: from_refs,
813        where_: selection.map(expr).transpose()?,
814        group_by,
815        having: having.map(expr).transpose()?,
816        lock: None,
817    })
818}
819
820fn wildcard_plain(o: &sp::WildcardAdditionalOptions) -> R<()> {
821    let sp::WildcardAdditionalOptions {
822        wildcard_token: _,
823        opt_ilike,
824        opt_exclude,
825        opt_except,
826        opt_replace,
827        opt_rename,
828        opt_alias,
829    } = o;
830    if opt_ilike.is_some()
831        || opt_exclude.is_some()
832        || opt_except.is_some()
833        || opt_replace.is_some()
834        || opt_rename.is_some()
835        || opt_alias.is_some()
836    {
837        return no("wildcard_option", "EXCLUDE/EXCEPT/REPLACE/RENAME on *");
838    }
839    Ok(())
840}
841
842fn select_item(it: sp::SelectItem) -> R<SelectItem> {
843    match it {
844        sp::SelectItem::UnnamedExpr(e) => Ok(SelectItem::Expr {
845            expr: expr(e)?,
846            alias: None,
847        }),
848        sp::SelectItem::ExprWithAlias { expr: e, alias } => Ok(SelectItem::Expr {
849            expr: expr(e)?,
850            alias: Some(ident(&alias)),
851        }),
852        sp::SelectItem::Wildcard(o) => {
853            wildcard_plain(&o)?;
854            Ok(SelectItem::Wildcard)
855        }
856        sp::SelectItem::QualifiedWildcard(kind, o) => {
857            wildcard_plain(&o)?;
858            match kind {
859                sp::SelectItemQualifiedWildcardKind::ObjectName(n) => {
860                    Ok(SelectItem::QualifiedWildcard(object_name(&n)?))
861                }
862                other => no("qualified_wildcard", &head(&other)),
863            }
864        }
865        other => no("select_item", &head(&other)),
866    }
867}
868
869fn alias_name(a: Option<sp::TableAlias>) -> R<Option<String>> {
870    match a {
871        None => Ok(None),
872        Some(sp::TableAlias {
873            explicit: _,
874            name,
875            columns,
876            at,
877        }) => {
878            if !columns.is_empty() || at.is_some() {
879                return no("table_alias_columns", "column list on a table alias");
880            }
881            Ok(Some(ident(&name)))
882        }
883    }
884}
885
886fn table_factor(t: sp::TableFactor) -> R<TableRef> {
887    match t {
888        sp::TableFactor::Table {
889            name,
890            alias,
891            args,
892            with_hints,
893            version,
894            with_ordinality,
895            partitions,
896            json_path,
897            sample,
898            index_hints,
899        } => {
900            // A table-valued function: positional expression arguments only.
901            if let Some(targs) = args {
902                let plain = with_hints.is_empty()
903                    && version.is_none()
904                    && !with_ordinality
905                    && partitions.is_empty()
906                    && json_path.is_none()
907                    && sample.is_none()
908                    && index_hints.is_empty()
909                    && targs.settings.is_none();
910                let nm = object_name(&name)?;
911                if plain && nm.0.len() == 1 {
912                    let mut out = Vec::new();
913                    for a in targs.args {
914                        match a {
915                            sp::FunctionArg::Unnamed(sp::FunctionArgExpr::Expr(e)) => {
916                                out.push(expr(e)?)
917                            }
918                            _ => {
919                                return no(
920                                    "table_function_arg",
921                                    "named or wildcard argument of a table function",
922                                );
923                            }
924                        }
925                    }
926                    return Ok(TableRef::Function {
927                        name: nm.0[0].clone(),
928                        args: out,
929                        alias: alias_name(alias)?,
930                    });
931                }
932                return no(
933                    "table_option",
934                    "table function args/hints/version/sample/partitions",
935                );
936            }
937            if !with_hints.is_empty()
938                || version.is_some()
939                || with_ordinality
940                || !partitions.is_empty()
941                || json_path.is_some()
942                || sample.is_some()
943                || !index_hints.is_empty()
944            {
945                return no(
946                    "table_option",
947                    "table function args/hints/version/sample/partitions",
948                );
949            }
950            Ok(TableRef::Table {
951                name: object_name(&name)?,
952                alias: alias_name(alias)?,
953            })
954        }
955        sp::TableFactor::Derived {
956            lateral,
957            subquery,
958            alias,
959            sample,
960        } => {
961            if lateral || sample.is_some() {
962                return no("derived_option", "LATERAL / TABLESAMPLE");
963            }
964            let alias = alias_name(alias)?.ok_or_else(|| ReadError::NotInIr {
965                construct: "unaliased_subquery".into(),
966                detail: "PostgreSQL requires an alias on a FROM subquery".into(),
967            })?;
968            Ok(TableRef::Subquery {
969                query: Box::new(query(*subquery)?),
970                alias,
971            })
972        }
973        other => no("table_factor", &head(&other)),
974    }
975}
976
977fn table_with_joins(t: sp::TableWithJoins) -> R<TableRef> {
978    let sp::TableWithJoins { relation, joins } = t;
979    let mut cur = table_factor(relation)?;
980    for j in joins {
981        let sp::Join {
982            relation,
983            global,
984            join_operator,
985        } = j;
986        if global {
987            return no("global_join", "GLOBAL JOIN");
988        }
989        let (kind, constraint) = match join_operator {
990            sp::JoinOperator::Join(c) | sp::JoinOperator::Inner(c) => (JoinKind::Inner, c),
991            sp::JoinOperator::Left(c) | sp::JoinOperator::LeftOuter(c) => (JoinKind::Left, c),
992            sp::JoinOperator::Right(c) | sp::JoinOperator::RightOuter(c) => (JoinKind::Right, c),
993            sp::JoinOperator::FullOuter(c) => (JoinKind::Full, c),
994            sp::JoinOperator::CrossJoin(c) => (JoinKind::Cross, c),
995            other => return no("join_kind", &head(&other)),
996        };
997        let on = match constraint {
998            sp::JoinConstraint::On(e) => Some(expr(e)?),
999            sp::JoinConstraint::None => None,
1000            sp::JoinConstraint::Using(_) => {
1001                return no("join_using", "JOIN .. USING is not in the IR yet");
1002            }
1003            sp::JoinConstraint::Natural => return no("natural_join", "NATURAL JOIN"),
1004        };
1005        cur = TableRef::Join {
1006            left: Box::new(cur),
1007            right: Box::new(table_factor(relation)?),
1008            kind,
1009            on,
1010        };
1011    }
1012    Ok(cur)
1013}
1014
1015fn simple_table(t: sp::TableWithJoins) -> R<(Name, Option<String>)> {
1016    let sp::TableWithJoins { relation, joins } = t;
1017    if !joins.is_empty() {
1018        return no("dml_target_join", "joins in a DML target");
1019    }
1020    match table_factor(relation)? {
1021        TableRef::Table { name, alias } => Ok((name, alias)),
1022        _ => no("dml_target", "DML target is not a plain table"),
1023    }
1024}
1025
1026fn returning(r: Option<Vec<sp::SelectItem>>) -> R<Vec<SelectItem>> {
1027    r.unwrap_or_default().into_iter().map(select_item).collect()
1028}
1029
1030fn assignments(a: Vec<sp::Assignment>) -> R<Vec<Assignment>> {
1031    let mut out = Vec::new();
1032    for sp::Assignment { target, value } in a {
1033        let column = match target {
1034            sp::AssignmentTarget::ColumnName(n) => single_name(&n)?,
1035            sp::AssignmentTarget::Tuple(_) => return no("tuple_assignment", "SET (a, b) = ..."),
1036        };
1037        out.push(Assignment {
1038            column,
1039            value: expr(value)?,
1040        });
1041    }
1042    Ok(out)
1043}
1044
1045fn insert(i: sp::Insert) -> R<Statement> {
1046    let sp::Insert {
1047        insert_token: _,
1048        optimizer_hints,
1049        or,
1050        ignore,
1051        into: _,
1052        table,
1053        table_alias,
1054        columns,
1055        overwrite,
1056        source,
1057        assignments: asg,
1058        partitioned,
1059        after_columns,
1060        has_table_keyword: _,
1061        on,
1062        returning: ret,
1063        output,
1064        replace_into,
1065        priority,
1066        insert_alias,
1067        settings,
1068        format_clause,
1069        multi_table_insert_type,
1070        multi_table_into_clauses,
1071        multi_table_when_clauses,
1072        multi_table_else_clause,
1073    } = i;
1074    if !optimizer_hints.is_empty()
1075        || or.is_some()
1076        || table_alias.is_some()
1077        || overwrite
1078        || !asg.is_empty()
1079        || partitioned.is_some()
1080        || !after_columns.is_empty()
1081        || output.is_some()
1082        || replace_into
1083        || priority.is_some()
1084        || insert_alias.is_some()
1085        || settings.is_some()
1086        || format_clause.is_some()
1087        || multi_table_insert_type.is_some()
1088        || !multi_table_into_clauses.is_empty()
1089        || !multi_table_when_clauses.is_empty()
1090        || multi_table_else_clause.is_some()
1091    {
1092        return no(
1093            "insert_option",
1094            "an INSERT form other than INSERT INTO t [(cols)] VALUES|query",
1095        );
1096    }
1097    let table = match table {
1098        sp::TableObject::TableName(n) => object_name(&n)?,
1099        other => return no("insert_target", &head(&other)),
1100    };
1101    let columns = columns.iter().map(single_name).collect::<R<Vec<_>>>()?;
1102    let source = match source {
1103        None => InsertSource::DefaultValues,
1104        Some(q) => {
1105            let q = *q;
1106            match *q.body {
1107                sp::SetExpr::Values(v)
1108                    if q.with.is_none()
1109                        && q.order_by.is_none()
1110                        && q.limit_clause.is_none()
1111                        && q.locks.is_empty() =>
1112                {
1113                    let sp::Values {
1114                        explicit_row,
1115                        value_keyword: _,
1116                        rows,
1117                    } = v;
1118                    if explicit_row {
1119                        return no("explicit_row", "ROW(..) in VALUES");
1120                    }
1121                    let mut out = Vec::new();
1122                    for r in rows {
1123                        out.push(r.content.into_iter().map(expr).collect::<R<Vec<_>>>()?);
1124                    }
1125                    InsertSource::Values(out)
1126                }
1127                body => {
1128                    let q2 = sp::Query {
1129                        body: Box::new(body),
1130                        ..q
1131                    };
1132                    InsertSource::Query(Box::new(query(q2)?))
1133                }
1134            }
1135        }
1136    };
1137    let on_conflict = match on {
1138        None => None,
1139        Some(sp::OnInsert::OnConflict(oc)) => {
1140            let sp::OnConflict {
1141                conflict_target,
1142                action,
1143            } = oc;
1144            let target = match conflict_target {
1145                None => Vec::new(),
1146                Some(sp::ConflictTarget::Columns(cols)) => cols.iter().map(ident).collect(),
1147                Some(sp::ConflictTarget::OnConstraint(_)) => {
1148                    return no("on_conflict_constraint", "ON CONFLICT ON CONSTRAINT");
1149                }
1150            };
1151            let action = match action {
1152                sp::OnConflictAction::DoNothing => ConflictAction::DoNothing,
1153                sp::OnConflictAction::DoUpdate(sp::DoUpdate {
1154                    assignments: a,
1155                    selection,
1156                }) => ConflictAction::DoUpdate {
1157                    set: assignments(a)?,
1158                    where_: selection.map(expr).transpose()?,
1159                },
1160            };
1161            Some(OnConflict { target, action })
1162        }
1163        Some(sp::OnInsert::DuplicateKeyUpdate(a)) => {
1164            // No conflict target: MySQL fires on ANY unique key. `VALUES(c)` is `excluded.c`.
1165            let set = assignments(a)?
1166                .into_iter()
1167                .map(|asg| Assignment {
1168                    column: asg.column,
1169                    value: excluded_refs(asg.value),
1170                })
1171                .collect();
1172            Some(OnConflict {
1173                target: vec![],
1174                action: ConflictAction::DoUpdate { set, where_: None },
1175            })
1176        }
1177        Some(other) => return no("on_insert", &head(&other)),
1178    };
1179    // MySQL `INSERT IGNORE`: a unique-key conflict skips the row, as `ON CONFLICT DO NOTHING`
1180    // (with no target) does. IGNORE also downgrades other errors, hence the allowance.
1181    let on_conflict = if ignore {
1182        if on_conflict.is_some() {
1183            return no(
1184                "insert_option",
1185                "IGNORE together with ON DUPLICATE KEY UPDATE",
1186            );
1187        }
1188        add_note(
1189            "insert_ignore",
1190            "INSERT IGNORE downgrades every error of the statement to a warning; ON CONFLICT DO NOTHING \
1191             skips unique-key conflicts only",
1192            Allowance::IgnoreCoversOnlyUniqueConflicts,
1193        );
1194        Some(OnConflict {
1195            target: vec![],
1196            action: ConflictAction::DoNothing,
1197        })
1198    } else {
1199        on_conflict
1200    };
1201    Ok(Statement::Insert(Insert {
1202        table,
1203        columns,
1204        source,
1205        on_conflict,
1206        returning: returning(ret)?,
1207    }))
1208}
1209
1210fn update(u: sp::Update) -> R<Statement> {
1211    let sp::Update {
1212        update_token: _,
1213        optimizer_hints,
1214        table,
1215        assignments: asg,
1216        from,
1217        selection,
1218        returning: ret,
1219        output,
1220        or,
1221        order_by,
1222        limit,
1223    } = u;
1224    if !optimizer_hints.is_empty()
1225        || output.is_some()
1226        || or.is_some()
1227        || !order_by.is_empty()
1228        || limit.is_some()
1229    {
1230        return no(
1231            "update_option",
1232            "UPDATE with hints/OUTPUT/OR/ORDER BY/LIMIT",
1233        );
1234    }
1235    let (table, alias) = simple_table(table)?;
1236    let from = match from {
1237        None => Vec::new(),
1238        Some(sp::UpdateTableFromKind::AfterSet(ts)) => ts
1239            .into_iter()
1240            .map(table_with_joins)
1241            .collect::<R<Vec<_>>>()?,
1242        Some(other) => return no("update_from_position", &head(&other)),
1243    };
1244    Ok(Statement::Update(Update {
1245        table,
1246        alias,
1247        set: assignments(asg)?,
1248        from,
1249        where_: selection.map(expr).transpose()?,
1250        returning: returning(ret)?,
1251    }))
1252}
1253
1254fn delete(d: sp::Delete) -> R<Statement> {
1255    let sp::Delete {
1256        delete_token: _,
1257        optimizer_hints,
1258        tables,
1259        from,
1260        using,
1261        selection,
1262        returning: ret,
1263        output,
1264        order_by,
1265        limit,
1266    } = d;
1267    if !optimizer_hints.is_empty()
1268        || !tables.is_empty()
1269        || output.is_some()
1270        || !order_by.is_empty()
1271        || limit.is_some()
1272    {
1273        return no(
1274            "delete_option",
1275            "DELETE with hints/multi-table/OUTPUT/ORDER BY/LIMIT",
1276        );
1277    }
1278    let ts = match from {
1279        sp::FromTable::WithFromKeyword(ts) | sp::FromTable::WithoutKeyword(ts) => ts,
1280    };
1281    if ts.len() != 1 {
1282        return no("delete_multi_target", "DELETE FROM a, b");
1283    }
1284    let (table, alias) = simple_table(ts.into_iter().next().unwrap())?;
1285    let using = using
1286        .unwrap_or_default()
1287        .into_iter()
1288        .map(table_with_joins)
1289        .collect::<R<Vec<_>>>()?;
1290    Ok(Statement::Delete(Delete {
1291        table,
1292        alias,
1293        using,
1294        where_: selection.map(expr).transpose()?,
1295        returning: returning(ret)?,
1296    }))
1297}
1298
1299fn boxed(e: sp::Expr) -> R<Box<Expr>> {
1300    Ok(Box::new(expr(e)?))
1301}
1302
1303fn expr(e: sp::Expr) -> R<Expr> {
1304    use sp::Expr as E;
1305    Ok(match e {
1306        E::Identifier(i) => Expr::Column(Name::simple(ident(&i))),
1307        E::CompoundIdentifier(parts) => Expr::Column(Name(parts.iter().map(ident).collect())),
1308        E::Value(v) => value(v.value)?,
1309        E::Nested(inner) => Expr::Nested(boxed(*inner)?),
1310        E::IsNull(x) => Expr::IsNull {
1311            expr: boxed(*x)?,
1312            negated: false,
1313        },
1314        E::IsNotNull(x) => Expr::IsNull {
1315            expr: boxed(*x)?,
1316            negated: true,
1317        },
1318        E::InList {
1319            expr: x,
1320            list,
1321            negated,
1322        } => Expr::InList {
1323            expr: boxed(*x)?,
1324            list: list.into_iter().map(expr).collect::<R<_>>()?,
1325            negated,
1326        },
1327        E::InSubquery {
1328            expr: x,
1329            subquery,
1330            negated,
1331        } => Expr::InSubquery {
1332            expr: boxed(*x)?,
1333            query: Box::new(query(*subquery)?),
1334            negated,
1335        },
1336        E::Between {
1337            expr: x,
1338            negated,
1339            low,
1340            high,
1341        } => Expr::Between {
1342            expr: boxed(*x)?,
1343            low: boxed(*low)?,
1344            high: boxed(*high)?,
1345            negated,
1346        },
1347        E::Like {
1348            negated,
1349            any,
1350            expr: x,
1351            pattern,
1352            escape_char,
1353        } => {
1354            if any {
1355                return no("like_option", "LIKE ANY");
1356            }
1357            let ci = rules().like_ci_default;
1358            if ci {
1359                add_note(
1360                    "like",
1361                    "the source LIKE is ASCII case-insensitive only while its connection setting is at the default",
1362                    Allowance::LikeCaseSensitivityIsConnectionSetting,
1363                );
1364            }
1365            if rules().name == "mysql" {
1366                add_note(
1367                    "like",
1368                    "the source LIKE follows the column collation; read as case-sensitive (a binary collation)",
1369                    Allowance::LikeCaseSensitivityIsConnectionSetting,
1370                );
1371            }
1372            like_expr(*x, *pattern, escape_char, negated, ci)?
1373        }
1374        E::ILike {
1375            negated,
1376            any,
1377            expr: x,
1378            pattern,
1379            escape_char,
1380        } => {
1381            if any {
1382                return no("like_option", "ILIKE ANY");
1383            }
1384            like_expr(*x, *pattern, escape_char, negated, true)?
1385        }
1386        E::UnaryOp { op, expr: x } => {
1387            let op = match op {
1388                sp::UnaryOperator::Not => UnaryOp::Not,
1389                sp::UnaryOperator::Minus => UnaryOp::Minus,
1390                sp::UnaryOperator::Plus => UnaryOp::Plus,
1391                other => return no("unary_operator", &head(&other)),
1392            };
1393            Expr::Unary {
1394                op,
1395                expr: boxed(*x)?,
1396            }
1397        }
1398        E::BinaryOp { left, op, right } => {
1399            use sp::BinaryOperator as B;
1400            let op = match op {
1401                B::Eq => BinaryOp::Eq,
1402                B::NotEq => BinaryOp::NotEq,
1403                B::Lt => BinaryOp::Lt,
1404                B::LtEq => BinaryOp::LtEq,
1405                B::Gt => BinaryOp::Gt,
1406                B::GtEq => BinaryOp::GtEq,
1407                B::And => BinaryOp::And,
1408                B::Or => BinaryOp::Or,
1409                B::Plus => BinaryOp::Plus,
1410                B::Minus => BinaryOp::Minus,
1411                B::Multiply => BinaryOp::Multiply,
1412                B::Divide => BinaryOp::Divide,
1413                B::Modulo => BinaryOp::Modulo,
1414                // MySQL's default sql_mode makes `||` a logical OR with OR's (lowest) precedence, but
1415                // sqlparser parses it as concatenation with concatenation's precedence, so the tree
1416                // is wrong either way: refuse instead of guessing.
1417                B::StringConcat if rules().name == "mysql" => {
1418                    return no(
1419                        "pipes_operator",
1420                        "MySQL `||` is logical OR (default sql_mode) but the parser gives it concatenation precedence; write OR, or CONCAT()",
1421                    );
1422                }
1423                B::StringConcat => BinaryOp::Concat,
1424                B::MyIntegerDivide => BinaryOp::IntDivide,
1425                B::Spaceship => BinaryOp::NullSafeEq,
1426                B::Arrow => BinaryOp::JsonGet,
1427                B::LongArrow => BinaryOp::JsonGetText,
1428                other => {
1429                    return no(
1430                        "binary_operator",
1431                        &format!(
1432                            "{} (JSON/array/regex/bitwise operators are not in the IR)",
1433                            head(&other)
1434                        ),
1435                    );
1436                }
1437            };
1438            if op == BinaryOp::Divide && rules().name == "mysql" {
1439                add_note(
1440                    "division",
1441                    "the source `/` is exact (DECIMAL) division; the target divides as double precision",
1442                    Allowance::DecimalDivisionAsFloat,
1443                );
1444                let l = Expr::Cast {
1445                    expr: boxed(*left)?,
1446                    ty: SqlType::Double,
1447                };
1448                return Ok(Expr::Binary {
1449                    op,
1450                    left: Box::new(l),
1451                    right: boxed(*right)?,
1452                });
1453            }
1454            // `ts +/- INTERVAL 'n unit'` is the same statement as `ts +/- make_interval(unit => n)`:
1455            // one IR form, so every target's interval arithmetic (and its refusal of months/years)
1456            // applies unchanged. `INTERVAL 'n unit' + ts` commutes for `+` only.
1457            match (op, *left, *right) {
1458                (BinaryOp::Plus | BinaryOp::Minus, l, E::Interval(iv)) => Expr::Binary {
1459                    op,
1460                    left: boxed(l)?,
1461                    right: Box::new(interval_call(iv)?),
1462                },
1463                (BinaryOp::Plus, E::Interval(iv), r) => Expr::Binary {
1464                    op,
1465                    left: boxed(r)?,
1466                    right: Box::new(interval_call(iv)?),
1467                },
1468                (op, l, r) => Expr::Binary {
1469                    op,
1470                    left: boxed(l)?,
1471                    right: boxed(r)?,
1472                },
1473            }
1474        }
1475        E::Case {
1476            case_token: _,
1477            end_token: _,
1478            operand,
1479            conditions,
1480            else_result,
1481        } => Expr::Case {
1482            operand: operand.map(|o| boxed(*o)).transpose()?,
1483            branches: conditions
1484                .into_iter()
1485                .map(|sp::CaseWhen { condition, result }| Ok((expr(condition)?, expr(result)?)))
1486                .collect::<R<_>>()?,
1487            else_: else_result.map(|o| boxed(*o)).transpose()?,
1488        },
1489        E::Cast {
1490            kind,
1491            expr: x,
1492            data_type,
1493            format,
1494        } => {
1495            if format.is_some() || !matches!(kind, sp::CastKind::Cast | sp::CastKind::DoubleColon) {
1496                return no("cast_form", "TRY_CAST / SAFE_CAST / CAST .. FORMAT");
1497            }
1498            Expr::Cast {
1499                expr: boxed(*x)?,
1500                ty: data_type_to_ir(&data_type)?,
1501            }
1502        }
1503        E::Trim {
1504            trim_where,
1505            trim_what,
1506            expr: x,
1507            trim_characters,
1508        } => {
1509            // `trim(chars FROM x)` / `trim(BOTH chars FROM x)`; the comma form is `trim_characters`.
1510            let side = match trim_where {
1511                None | Some(sp::TrimWhereField::Both) => TrimSide::Both,
1512                Some(sp::TrimWhereField::Leading) => TrimSide::Leading,
1513                Some(sp::TrimWhereField::Trailing) => TrimSide::Trailing,
1514            };
1515            let chars = match (trim_what, trim_characters) {
1516                (Some(w), None) => Some(boxed(*w)?),
1517                (None, None) => None,
1518                (None, Some(mut cs)) if cs.len() == 1 => Some(boxed(cs.remove(0))?),
1519                _ => return no("trim_form", "TRIM with several character arguments"),
1520            };
1521            Expr::Trim {
1522                expr: boxed(*x)?,
1523                side,
1524                chars,
1525            }
1526        }
1527        E::Substring {
1528            expr: x,
1529            substring_from,
1530            substring_for,
1531            special: _,
1532            shorthand: _,
1533        } => {
1534            if rules().name == "mysql" {
1535                // From position 1 and a non-negative length the engines agree; any other start
1536                // (0, negative, an expression) or length is the caller's stated assumption.
1537                let positive = |e: &E, min: u64| {
1538                    matches!(
1539                        e,
1540                        E::Value(v) if matches!(&v.value, sp::Value::Number(n, _)
1541                            if n.parse::<u64>().is_ok_and(|x| x >= min))
1542                    )
1543                };
1544                let start_ok = substring_from.as_deref().is_some_and(|f| positive(f, 1));
1545                let len_ok = substring_for.as_deref().is_none_or(|l| positive(l, 0));
1546                if !(start_ok && len_ok) {
1547                    add_note(
1548                        "fn:substr.start_position",
1549                        "MySQL SUBSTRING counts a start of 0 as empty and a negative start from the end; \
1550                         the target does not",
1551                        Allowance::SubstringStartAssumedPositive,
1552                    );
1553                }
1554            }
1555            // `substring(x FROM a FOR b)` and `substring(x, a, b)` are `substr(x, a, b)`.
1556            let mut args = vec![FunctionArg::Expr(expr(*x)?)];
1557            match (substring_from, substring_for) {
1558                (Some(f), Some(l)) => {
1559                    args.push(FunctionArg::Expr(expr(*f)?));
1560                    args.push(FunctionArg::Expr(expr(*l)?));
1561                }
1562                (Some(f), None) => args.push(FunctionArg::Expr(expr(*f)?)),
1563                (None, Some(l)) => {
1564                    // FOR without FROM starts at position 1.
1565                    args.push(FunctionArg::Expr(Expr::Literal(Literal::Number(
1566                        "1".into(),
1567                    ))));
1568                    args.push(FunctionArg::Expr(expr(*l)?));
1569                }
1570                (None, None) => return no("substring_form", "SUBSTRING without FROM or FOR"),
1571            }
1572            Expr::Function(Function {
1573                name: "substr".into(),
1574                args,
1575                distinct: false,
1576                star: false,
1577                filter: None,
1578                over: None,
1579                order_by: vec![],
1580            })
1581        }
1582        E::AnyOp {
1583            left,
1584            compare_op,
1585            right,
1586            is_some: _,
1587        } => {
1588            if !matches!(compare_op, sp::BinaryOperator::Eq) {
1589                return no("any_operator", "only `= ANY(..)` is in the IR");
1590            }
1591            quantified(*left, *right, false)?
1592        }
1593        E::AllOp {
1594            left,
1595            compare_op,
1596            right,
1597        } => {
1598            if !matches!(compare_op, sp::BinaryOperator::NotEq) {
1599                return no("all_operator", "only `<> ALL(..)` is in the IR");
1600            }
1601            quantified(*left, *right, true)?
1602        }
1603        E::Exists { subquery, negated } => Expr::Exists {
1604            query: Box::new(query(*subquery)?),
1605            negated,
1606        },
1607        E::Subquery(q) => Expr::Subquery(Box::new(query(*q)?)),
1608        E::Extract {
1609            field,
1610            syntax: _,
1611            expr: x,
1612        } => Expr::Extract {
1613            field: field.to_string().to_ascii_lowercase(),
1614            expr: boxed(*x)?,
1615        },
1616        E::Array(arr) => Expr::ArrayLiteral(arr.elem.into_iter().map(expr).collect::<R<Vec<_>>>()?),
1617        E::AtTimeZone {
1618            timestamp,
1619            time_zone,
1620        } => Expr::AtTimeZone {
1621            expr: boxed(*timestamp)?,
1622            zone: boxed(*time_zone)?,
1623        },
1624        E::Function(f) => function(f)?,
1625        other => return no("expression", &head(&other)),
1626    })
1627}
1628
1629/// `x = ANY(rhs)` / `x <> ALL(rhs)`: an array parameter, an array literal, or a subquery.
1630fn quantified(left: sp::Expr, right: sp::Expr, negated: bool) -> R<Expr> {
1631    let lhs = boxed(left)?;
1632    // `$1::int[]` is only a type hint on the parameter.
1633    let mut right = right;
1634    if let sp::Expr::Cast {
1635        kind: sp::CastKind::Cast | sp::CastKind::DoubleColon,
1636        expr: inner,
1637        data_type: sp::DataType::Array(_),
1638        format: None,
1639    } = right.clone()
1640    {
1641        right = *inner;
1642    }
1643    match right {
1644        sp::Expr::Value(v) => match v.value {
1645            sp::Value::Placeholder(_) => match value(v.value)? {
1646                Expr::Param(n) => Ok(Expr::AnyParam {
1647                    expr: lhs,
1648                    param: n,
1649                    negated,
1650                }),
1651                _ => no("array_param", "expected a numbered parameter"),
1652            },
1653            other => no("array_operand", &head(&other)),
1654        },
1655        sp::Expr::Array(sp::Array { elem, named: _ }) => Ok(Expr::InList {
1656            expr: lhs,
1657            list: elem.into_iter().map(expr).collect::<R<Vec<_>>>()?,
1658            negated,
1659        }),
1660        sp::Expr::Subquery(q) => Ok(Expr::InSubquery {
1661            expr: lhs,
1662            query: Box::new(query(*q)?),
1663            negated,
1664        }),
1665        other => no("array_operand", &head(&other)),
1666    }
1667}
1668
1669/// MySQL's `VALUES(col)` inside `ON DUPLICATE KEY UPDATE` refers to the row proposed for insertion.
1670fn excluded_refs(e: Expr) -> Expr {
1671    match e {
1672        Expr::Function(f) if f.name == "values" && f.args.len() == 1 => {
1673            match f.args.into_iter().next() {
1674                Some(FunctionArg::Expr(Expr::Column(n))) => {
1675                    Expr::Column(Name(vec!["excluded".to_string(), n.last().to_string()]))
1676                }
1677                other => Expr::Function(Function {
1678                    name: "values".into(),
1679                    args: other.into_iter().collect(),
1680                    distinct: false,
1681                    star: false,
1682                    filter: None,
1683                    over: None,
1684                    order_by: vec![],
1685                }),
1686            }
1687        }
1688        Expr::Binary { op, left, right } => Expr::Binary {
1689            op,
1690            left: Box::new(excluded_refs(*left)),
1691            right: Box::new(excluded_refs(*right)),
1692        },
1693        Expr::Nested(x) => Expr::Nested(Box::new(excluded_refs(*x))),
1694        Expr::Unary { op, expr } => Expr::Unary {
1695            op,
1696            expr: Box::new(excluded_refs(*expr)),
1697        },
1698        other => other,
1699    }
1700}
1701
1702fn like_expr(
1703    x: sp::Expr,
1704    pattern: sp::Expr,
1705    escape_char: Option<Box<sp::Expr>>,
1706    negated: bool,
1707    case_insensitive: bool,
1708) -> R<Expr> {
1709    let escape = match escape_char {
1710        Some(e) => Some(boxed(*e)?),
1711        None if rules().like_default_escape => {
1712            Some(Box::new(Expr::Literal(Literal::String("\\".into()))))
1713        }
1714        None => None,
1715    };
1716    Ok(Expr::Like {
1717        expr: boxed(x)?,
1718        pattern: boxed(pattern)?,
1719        negated,
1720        case_insensitive,
1721        escape,
1722    })
1723}
1724
1725fn value(v: sp::Value) -> R<Expr> {
1726    Ok(match v {
1727        sp::Value::Number(n, long) => {
1728            if long {
1729                return no("numeric_suffix", "L suffix");
1730            }
1731            Expr::Literal(Literal::Number(n))
1732        }
1733        sp::Value::SingleQuotedString(s) => Expr::Literal(Literal::String(s)),
1734        // The tokenizer has already resolved the backslash escapes of `E'..'`.
1735        sp::Value::EscapedStringLiteral(s) => Expr::Literal(Literal::String(s)),
1736        sp::Value::Boolean(b) if rules().name == "mysql" => {
1737            // MySQL TRUE/FALSE are the integers 1 and 0.
1738            Expr::Literal(Literal::Number(if b { "1" } else { "0" }.into()))
1739        }
1740        sp::Value::Boolean(b) => Expr::Literal(Literal::Bool(b)),
1741        sp::Value::Null => Expr::Literal(Literal::Null),
1742        sp::Value::Placeholder(p) => {
1743            let bad = || ReadError::NotInIr {
1744                construct: "placeholder".into(),
1745                detail: format!(
1746                    "`{p}` is not a placeholder form of dialect `{}`",
1747                    rules().name
1748                ),
1749            };
1750            if rules().question_placeholders {
1751                if p == "?" {
1752                    Expr::Param(bump_param(None, None))
1753                } else if let Some(d) = p.strip_prefix('?') {
1754                    let n = d.parse::<u32>().ok().filter(|n| *n >= 1).ok_or_else(bad)?;
1755                    Expr::Param(bump_param(Some(n), None))
1756                } else if let Some(nm) = p.strip_prefix([':', '@', '$']) {
1757                    Expr::Param(bump_param(None, Some(nm)))
1758                } else {
1759                    return Err(bad());
1760                }
1761            } else {
1762                let n = p
1763                    .strip_prefix('$')
1764                    .and_then(|d| d.parse::<u32>().ok())
1765                    .filter(|n| *n >= 1)
1766                    .ok_or_else(bad)?;
1767                Expr::Param(n)
1768            }
1769        }
1770        other => return no("literal", &head(&other)),
1771    })
1772}
1773
1774fn function(f: sp::Function) -> R<Expr> {
1775    let sp::Function {
1776        name,
1777        uses_odbc_syntax,
1778        parameters,
1779        args,
1780        within_group,
1781        filter,
1782        null_treatment,
1783        over,
1784    } = f;
1785    if uses_odbc_syntax
1786        || !matches!(parameters, sp::FunctionArguments::None)
1787        || !within_group.is_empty()
1788        || null_treatment.is_some()
1789    {
1790        return no(
1791            "function_option",
1792            "ODBC syntax/parameters/WITHIN GROUP/FILTER/IGNORE NULLS",
1793        );
1794    }
1795    let over = match over {
1796        None => None,
1797        Some(sp::WindowType::NamedWindow(_)) => {
1798            return no(
1799                "named_window",
1800                "OVER w (a WINDOW clause) is not in the IR yet",
1801            );
1802        }
1803        Some(sp::WindowType::WindowSpec(spec)) => Some(window_spec(spec)?),
1804    };
1805    let filter = filter.map(|f| expr(*f).map(Box::new)).transpose()?;
1806    let name = object_name(&name)?;
1807    let base = name.last().to_string();
1808    if name.0.len() != 1 {
1809        return no("qualified_function", &name.0.join("."));
1810    }
1811    let args = match args {
1812        // `CURRENT_TIMESTAMP` is a function without parentheses.
1813        sp::FunctionArguments::None => {
1814            if base == "current_timestamp" {
1815                if rules().name == "sqlite" {
1816                    add_note(
1817                        "fn:now",
1818                        "the source CURRENT_TIMESTAMP is text 'YYYY-MM-DD HH:MM:SS' (UTC), not a timestamptz",
1819                        Allowance::NowAsText,
1820                    );
1821                }
1822                return Ok(Expr::Function(Function {
1823                    name: "now".to_string(),
1824                    args: vec![],
1825                    distinct: false,
1826                    star: false,
1827                    filter: None,
1828                    over: None,
1829                    order_by: vec![],
1830                }));
1831            }
1832            // The session identity keywords are zero-argument functions.
1833            if matches!(
1834                base.as_str(),
1835                "current_user" | "session_user" | "current_role"
1836            ) {
1837                return Ok(Expr::Function(Function {
1838                    name: base,
1839                    args: vec![],
1840                    distinct: false,
1841                    star: false,
1842                    filter: None,
1843                    over: None,
1844                    order_by: vec![],
1845                }));
1846            }
1847            // `CURRENT_DATE`: a date in the session time zone (PostgreSQL, MySQL), text of the UTC
1848            // date on SQLite.
1849            if base == "current_date" {
1850                if rules().name == "sqlite" {
1851                    add_note(
1852                        "fn:current_date",
1853                        "the source CURRENT_DATE is text 'YYYY-MM-DD' (the UTC date), not a date",
1854                        Allowance::DateAsText,
1855                    );
1856                }
1857                return Ok(Expr::Function(Function {
1858                    name: "current_date".to_string(),
1859                    args: vec![],
1860                    distinct: false,
1861                    star: false,
1862                    filter: None,
1863                    over: None,
1864                    order_by: vec![],
1865                }));
1866            }
1867            return no("bare_function", &format!("`{base}` without parentheses"));
1868        }
1869        sp::FunctionArguments::Subquery(q) if base == "array" => {
1870            return Ok(Expr::ArraySubquery(Box::new(query(*q)?)));
1871        }
1872        sp::FunctionArguments::Subquery(_) => {
1873            return no("function_subquery", "function over a subquery");
1874        }
1875        sp::FunctionArguments::List(l) => l,
1876    };
1877    let sp::FunctionArgumentList {
1878        duplicate_treatment,
1879        args,
1880        clauses,
1881    } = args;
1882    let mut agg_order = Vec::new();
1883    let mut separator: Option<String> = None;
1884    for cl in clauses {
1885        match cl {
1886            sp::FunctionArgumentClause::OrderBy(items) => {
1887                for it in items {
1888                    agg_order.push(order_item(it)?);
1889                }
1890            }
1891            sp::FunctionArgumentClause::Separator(v) => match v.value {
1892                sp::Value::SingleQuotedString(s) => separator = Some(s),
1893                other => return no("separator", &head(&other)),
1894            },
1895            other => {
1896                return no(
1897                    "function_clause",
1898                    &format!("{} inside a function call", head(&other)),
1899                );
1900            }
1901        }
1902    }
1903    let distinct = match duplicate_treatment {
1904        None | Some(sp::DuplicateTreatment::All) => false,
1905        Some(sp::DuplicateTreatment::Distinct) => true,
1906    };
1907    let mut out = Vec::new();
1908    let mut star = false;
1909    for a in args {
1910        match a {
1911            sp::FunctionArg::Unnamed(sp::FunctionArgExpr::Expr(e)) => {
1912                out.push(FunctionArg::Expr(expr(e)?))
1913            }
1914            sp::FunctionArg::Unnamed(sp::FunctionArgExpr::Wildcard) => star = true,
1915            sp::FunctionArg::Named {
1916                name,
1917                arg: sp::FunctionArgExpr::Expr(e),
1918                operator: sp::FunctionArgOperator::RightArrow,
1919            } => out.push(FunctionArg::Named {
1920                name: ident(&name),
1921                value: expr(e)?,
1922            }),
1923            // sqlparser produces `ExprNamed` when the name is parsed as an expression.
1924            sp::FunctionArg::ExprNamed {
1925                name: sp::Expr::Identifier(name),
1926                arg: sp::FunctionArgExpr::Expr(e),
1927                operator: sp::FunctionArgOperator::RightArrow,
1928            } => out.push(FunctionArg::Named {
1929                name: ident(&name),
1930                value: expr(e)?,
1931            }),
1932            other => return no("function_argument", &head(&other)),
1933        }
1934    }
1935    let mut name = base;
1936    if name == "current_timestamp" {
1937        name = "now".to_string();
1938    }
1939    if rules().name == "mysql" {
1940        match name.as_str() {
1941            "ifnull" => name = "coalesce".to_string(),
1942            "group_concat" => {
1943                name = "string_agg".to_string();
1944                out.push(FunctionArg::Expr(Expr::Literal(Literal::String(
1945                    separator.clone().unwrap_or_else(|| ",".to_string()),
1946                ))));
1947                add_note(
1948                    "fn:string_agg",
1949                    "the source GROUP_CONCAT truncates silently at group_concat_max_len",
1950                    Allowance::AggregateStringTruncation,
1951                );
1952            }
1953            // MySQL LENGTH() counts bytes.
1954            "length" => name = "octet_length".to_string(),
1955            "concat" if out.len() >= 2 => {
1956                let mut it = out.into_iter().map(|a| match a {
1957                    FunctionArg::Expr(e) => e,
1958                    FunctionArg::Named { value, .. } => value,
1959                });
1960                let first = it.next().expect("two arguments");
1961                let chain = it.fold(first, |l, r| Expr::Binary {
1962                    op: BinaryOp::Concat,
1963                    left: Box::new(l),
1964                    right: Box::new(r),
1965                });
1966                return Ok(chain);
1967            }
1968            "if" if out.len() == 3 => {
1969                let mut it = out.into_iter().map(|a| match a {
1970                    FunctionArg::Expr(e) => e,
1971                    FunctionArg::Named { value, .. } => value,
1972                });
1973                let (c, t, f) = (it.next().unwrap(), it.next().unwrap(), it.next().unwrap());
1974                return Ok(Expr::Case {
1975                    operand: None,
1976                    branches: vec![(c, t)],
1977                    else_: Some(Box::new(f)),
1978                });
1979            }
1980            _ => {}
1981        }
1982    }
1983    if rules().name == "sqlite" {
1984        match name.as_str() {
1985            "ifnull" => name = "coalesce".to_string(),
1986            "group_concat" => {
1987                name = "string_agg".to_string();
1988                if out.len() == 1 {
1989                    out.push(FunctionArg::Expr(Expr::Literal(Literal::String(
1990                        ",".into(),
1991                    ))));
1992                }
1993            }
1994            _ => {}
1995        }
1996    }
1997    Ok(Expr::Function(Function {
1998        name,
1999        args: out,
2000        distinct,
2001        star,
2002        filter,
2003        over,
2004        order_by: agg_order,
2005    }))
2006}
2007
2008/// `INTERVAL '3 days'` / `INTERVAL 3 DAY` as `make_interval(days => 3)`. Only a single `<number> <unit>`
2009/// is represented; compound intervals (`'1 day 2 hours'`), ranges (`DAY TO SECOND`) and unit-less
2010/// strings are not in the IR. Months and years are read (and then refused by the targets, whose
2011/// month arithmetic differs), not guessed.
2012fn interval_call(iv: sp::Interval) -> R<Expr> {
2013    let sp::Interval {
2014        value,
2015        leading_field,
2016        leading_precision,
2017        last_field,
2018        fractional_seconds_precision,
2019    } = iv;
2020    if leading_precision.is_some() || last_field.is_some() || fractional_seconds_precision.is_some()
2021    {
2022        return no(
2023            "interval",
2024            "interval ranges and precisions are not in the IR",
2025        );
2026    }
2027    let (amount, unit) = match (*value, leading_field) {
2028        // MySQL `INTERVAL 3 DAY`
2029        (sp::Expr::Value(v), Some(field)) => (
2030            match v.value {
2031                sp::Value::Number(n, _) => n,
2032                other => return no("interval", &head(&other)),
2033            },
2034            field.to_string(),
2035        ),
2036        // PostgreSQL / standard `INTERVAL '3 days'`
2037        (sp::Expr::Value(v), None) => match v.value {
2038            sp::Value::SingleQuotedString(s) => {
2039                let mut it = s.split_whitespace();
2040                match (it.next(), it.next(), it.next()) {
2041                    (Some(a), Some(u), None) => (a.to_string(), u.to_string()),
2042                    _ => {
2043                        return no(
2044                            "interval",
2045                            "only a single `<number> <unit>` interval is in the IR",
2046                        );
2047                    }
2048                }
2049            }
2050            other => return no("interval", &head(&other)),
2051        },
2052        (other, _) => return no("interval", &head(&other)),
2053    };
2054    let numeric = !amount.is_empty()
2055        && amount
2056            .trim_start_matches('-')
2057            .chars()
2058            .all(|c| c.is_ascii_digit() || c == '.')
2059        && amount
2060            .trim_start_matches('-')
2061            .chars()
2062            .any(|c| c.is_ascii_digit());
2063    if !numeric {
2064        return no("interval", "the interval amount is not a plain number");
2065    }
2066    let unit = match unit.to_ascii_lowercase().trim_end_matches('s') {
2067        "second" | "sec" => "secs",
2068        "minute" | "min" => "mins",
2069        "hour" => "hours",
2070        "day" => "days",
2071        "week" => "weeks",
2072        "month" | "mon" => "months",
2073        "year" => "years",
2074        other => return no("interval", &format!("unit `{other}` is not in the IR")),
2075    };
2076    Ok(Expr::Function(Function {
2077        name: "make_interval".to_string(),
2078        args: vec![FunctionArg::Named {
2079            name: unit.to_string(),
2080            value: Expr::Literal(Literal::Number(amount)),
2081        }],
2082        distinct: false,
2083        star: false,
2084        filter: None,
2085        over: None,
2086        order_by: vec![],
2087    }))
2088}
2089
2090fn window_spec(w: sp::WindowSpec) -> R<Window> {
2091    let sp::WindowSpec {
2092        window_name,
2093        partition_by,
2094        order_by,
2095        window_frame,
2096    } = w;
2097    if window_name.is_some() {
2098        return no("named_window", "OVER (w ..) refinement of a named window");
2099    }
2100    let frame = match window_frame {
2101        None => None,
2102        Some(sp::WindowFrame {
2103            units,
2104            start_bound,
2105            end_bound,
2106        }) => {
2107            let bound = |b: sp::WindowFrameBound| -> R<FrameBound> {
2108                Ok(match b {
2109                    sp::WindowFrameBound::CurrentRow => FrameBound::CurrentRow,
2110                    sp::WindowFrameBound::Preceding(e) => {
2111                        FrameBound::Preceding(e.map(|x| expr(*x).map(Box::new)).transpose()?)
2112                    }
2113                    sp::WindowFrameBound::Following(e) => {
2114                        FrameBound::Following(e.map(|x| expr(*x).map(Box::new)).transpose()?)
2115                    }
2116                })
2117            };
2118            Some(Frame {
2119                units: match units {
2120                    sp::WindowFrameUnits::Rows => FrameUnits::Rows,
2121                    sp::WindowFrameUnits::Range => FrameUnits::Range,
2122                    sp::WindowFrameUnits::Groups => FrameUnits::Groups,
2123                },
2124                start: bound(start_bound)?,
2125                end: end_bound.map(bound).transpose()?,
2126            })
2127        }
2128    };
2129    Ok(Window {
2130        partition_by: partition_by.into_iter().map(expr).collect::<R<_>>()?,
2131        order_by: order_by.into_iter().map(order_item).collect::<R<_>>()?,
2132        frame,
2133    })
2134}
2135
2136fn data_type_to_ir(t: &sp::DataType) -> R<SqlType> {
2137    use sp::DataType as D;
2138    Ok(match t {
2139        D::SmallInt(_) | D::Int2(_) => SqlType::SmallInt,
2140        D::Int(_) | D::Integer(_) | D::Int4(_) => SqlType::Integer,
2141        D::BigInt(_) | D::Int8(_) => SqlType::BigInt,
2142        D::Real | D::Float4 => SqlType::Real,
2143        D::Double(_) | D::DoublePrecision | D::Float8 => SqlType::Double,
2144        D::Numeric(info) | D::Decimal(info) => match info {
2145            sp::ExactNumberInfo::None => SqlType::Numeric {
2146                precision: None,
2147                scale: None,
2148            },
2149            sp::ExactNumberInfo::Precision(p) => SqlType::Numeric {
2150                precision: Some(*p as u32),
2151                scale: None,
2152            },
2153            sp::ExactNumberInfo::PrecisionAndScale(p, s) => SqlType::Numeric {
2154                precision: Some(*p as u32),
2155                scale: Some(*s as u32),
2156            },
2157        },
2158        D::Text => SqlType::Text,
2159        D::Signed | D::SignedInteger | D::Unsigned | D::UnsignedInteger => SqlType::BigInt,
2160        D::Char(None) | D::Character(None) => SqlType::Text,
2161        D::Datetime(None) => SqlType::Timestamp { with_tz: false },
2162        D::Varchar(None) | D::CharVarying(None) => SqlType::Varchar(None),
2163        D::Varchar(Some(sp::CharacterLength::IntegerLength { length, unit: None }))
2164        | D::CharVarying(Some(sp::CharacterLength::IntegerLength { length, unit: None })) => {
2165            SqlType::Varchar(Some(*length as u32))
2166        }
2167        D::Boolean | D::Bool => SqlType::Boolean,
2168        D::Date => SqlType::Date,
2169        D::Timestamp(None, tz) => SqlType::Timestamp {
2170            with_tz: match tz {
2171                sp::TimezoneInfo::None | sp::TimezoneInfo::WithoutTimeZone => false,
2172                sp::TimezoneInfo::WithTimeZone | sp::TimezoneInfo::Tz => true,
2173            },
2174        },
2175        D::Uuid => SqlType::Uuid,
2176        D::Bytea => SqlType::Bytes,
2177        D::JSON => SqlType::Json,
2178        D::JSONB => SqlType::Jsonb,
2179        other => return no("data_type", &format!("{other}")),
2180    })
2181}