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            if args.is_some()
901                || !with_hints.is_empty()
902                || version.is_some()
903                || with_ordinality
904                || !partitions.is_empty()
905                || json_path.is_some()
906                || sample.is_some()
907                || !index_hints.is_empty()
908            {
909                return no(
910                    "table_option",
911                    "table function args/hints/version/sample/partitions",
912                );
913            }
914            Ok(TableRef::Table {
915                name: object_name(&name)?,
916                alias: alias_name(alias)?,
917            })
918        }
919        sp::TableFactor::Derived {
920            lateral,
921            subquery,
922            alias,
923            sample,
924        } => {
925            if lateral || sample.is_some() {
926                return no("derived_option", "LATERAL / TABLESAMPLE");
927            }
928            let alias = alias_name(alias)?.ok_or_else(|| ReadError::NotInIr {
929                construct: "unaliased_subquery".into(),
930                detail: "PostgreSQL requires an alias on a FROM subquery".into(),
931            })?;
932            Ok(TableRef::Subquery {
933                query: Box::new(query(*subquery)?),
934                alias,
935            })
936        }
937        other => no("table_factor", &head(&other)),
938    }
939}
940
941fn table_with_joins(t: sp::TableWithJoins) -> R<TableRef> {
942    let sp::TableWithJoins { relation, joins } = t;
943    let mut cur = table_factor(relation)?;
944    for j in joins {
945        let sp::Join {
946            relation,
947            global,
948            join_operator,
949        } = j;
950        if global {
951            return no("global_join", "GLOBAL JOIN");
952        }
953        let (kind, constraint) = match join_operator {
954            sp::JoinOperator::Join(c) | sp::JoinOperator::Inner(c) => (JoinKind::Inner, c),
955            sp::JoinOperator::Left(c) | sp::JoinOperator::LeftOuter(c) => (JoinKind::Left, c),
956            sp::JoinOperator::Right(c) | sp::JoinOperator::RightOuter(c) => (JoinKind::Right, c),
957            sp::JoinOperator::FullOuter(c) => (JoinKind::Full, c),
958            sp::JoinOperator::CrossJoin(c) => (JoinKind::Cross, c),
959            other => return no("join_kind", &head(&other)),
960        };
961        let on = match constraint {
962            sp::JoinConstraint::On(e) => Some(expr(e)?),
963            sp::JoinConstraint::None => None,
964            sp::JoinConstraint::Using(_) => {
965                return no("join_using", "JOIN .. USING is not in the IR yet");
966            }
967            sp::JoinConstraint::Natural => return no("natural_join", "NATURAL JOIN"),
968        };
969        cur = TableRef::Join {
970            left: Box::new(cur),
971            right: Box::new(table_factor(relation)?),
972            kind,
973            on,
974        };
975    }
976    Ok(cur)
977}
978
979fn simple_table(t: sp::TableWithJoins) -> R<(Name, Option<String>)> {
980    let sp::TableWithJoins { relation, joins } = t;
981    if !joins.is_empty() {
982        return no("dml_target_join", "joins in a DML target");
983    }
984    match table_factor(relation)? {
985        TableRef::Table { name, alias } => Ok((name, alias)),
986        _ => no("dml_target", "DML target is not a plain table"),
987    }
988}
989
990fn returning(r: Option<Vec<sp::SelectItem>>) -> R<Vec<SelectItem>> {
991    r.unwrap_or_default().into_iter().map(select_item).collect()
992}
993
994fn assignments(a: Vec<sp::Assignment>) -> R<Vec<Assignment>> {
995    let mut out = Vec::new();
996    for sp::Assignment { target, value } in a {
997        let column = match target {
998            sp::AssignmentTarget::ColumnName(n) => single_name(&n)?,
999            sp::AssignmentTarget::Tuple(_) => return no("tuple_assignment", "SET (a, b) = ..."),
1000        };
1001        out.push(Assignment {
1002            column,
1003            value: expr(value)?,
1004        });
1005    }
1006    Ok(out)
1007}
1008
1009fn insert(i: sp::Insert) -> R<Statement> {
1010    let sp::Insert {
1011        insert_token: _,
1012        optimizer_hints,
1013        or,
1014        ignore,
1015        into: _,
1016        table,
1017        table_alias,
1018        columns,
1019        overwrite,
1020        source,
1021        assignments: asg,
1022        partitioned,
1023        after_columns,
1024        has_table_keyword: _,
1025        on,
1026        returning: ret,
1027        output,
1028        replace_into,
1029        priority,
1030        insert_alias,
1031        settings,
1032        format_clause,
1033        multi_table_insert_type,
1034        multi_table_into_clauses,
1035        multi_table_when_clauses,
1036        multi_table_else_clause,
1037    } = i;
1038    if !optimizer_hints.is_empty()
1039        || or.is_some()
1040        || table_alias.is_some()
1041        || overwrite
1042        || !asg.is_empty()
1043        || partitioned.is_some()
1044        || !after_columns.is_empty()
1045        || output.is_some()
1046        || replace_into
1047        || priority.is_some()
1048        || insert_alias.is_some()
1049        || settings.is_some()
1050        || format_clause.is_some()
1051        || multi_table_insert_type.is_some()
1052        || !multi_table_into_clauses.is_empty()
1053        || !multi_table_when_clauses.is_empty()
1054        || multi_table_else_clause.is_some()
1055    {
1056        return no(
1057            "insert_option",
1058            "an INSERT form other than INSERT INTO t [(cols)] VALUES|query",
1059        );
1060    }
1061    let table = match table {
1062        sp::TableObject::TableName(n) => object_name(&n)?,
1063        other => return no("insert_target", &head(&other)),
1064    };
1065    let columns = columns.iter().map(single_name).collect::<R<Vec<_>>>()?;
1066    let source = match source {
1067        None => InsertSource::DefaultValues,
1068        Some(q) => {
1069            let q = *q;
1070            match *q.body {
1071                sp::SetExpr::Values(v)
1072                    if q.with.is_none()
1073                        && q.order_by.is_none()
1074                        && q.limit_clause.is_none()
1075                        && q.locks.is_empty() =>
1076                {
1077                    let sp::Values {
1078                        explicit_row,
1079                        value_keyword: _,
1080                        rows,
1081                    } = v;
1082                    if explicit_row {
1083                        return no("explicit_row", "ROW(..) in VALUES");
1084                    }
1085                    let mut out = Vec::new();
1086                    for r in rows {
1087                        out.push(r.content.into_iter().map(expr).collect::<R<Vec<_>>>()?);
1088                    }
1089                    InsertSource::Values(out)
1090                }
1091                body => {
1092                    let q2 = sp::Query {
1093                        body: Box::new(body),
1094                        ..q
1095                    };
1096                    InsertSource::Query(Box::new(query(q2)?))
1097                }
1098            }
1099        }
1100    };
1101    let on_conflict = match on {
1102        None => None,
1103        Some(sp::OnInsert::OnConflict(oc)) => {
1104            let sp::OnConflict {
1105                conflict_target,
1106                action,
1107            } = oc;
1108            let target = match conflict_target {
1109                None => Vec::new(),
1110                Some(sp::ConflictTarget::Columns(cols)) => cols.iter().map(ident).collect(),
1111                Some(sp::ConflictTarget::OnConstraint(_)) => {
1112                    return no("on_conflict_constraint", "ON CONFLICT ON CONSTRAINT");
1113                }
1114            };
1115            let action = match action {
1116                sp::OnConflictAction::DoNothing => ConflictAction::DoNothing,
1117                sp::OnConflictAction::DoUpdate(sp::DoUpdate {
1118                    assignments: a,
1119                    selection,
1120                }) => ConflictAction::DoUpdate {
1121                    set: assignments(a)?,
1122                    where_: selection.map(expr).transpose()?,
1123                },
1124            };
1125            Some(OnConflict { target, action })
1126        }
1127        Some(sp::OnInsert::DuplicateKeyUpdate(a)) => {
1128            // No conflict target: MySQL fires on ANY unique key. `VALUES(c)` is `excluded.c`.
1129            let set = assignments(a)?
1130                .into_iter()
1131                .map(|asg| Assignment {
1132                    column: asg.column,
1133                    value: excluded_refs(asg.value),
1134                })
1135                .collect();
1136            Some(OnConflict {
1137                target: vec![],
1138                action: ConflictAction::DoUpdate { set, where_: None },
1139            })
1140        }
1141        Some(other) => return no("on_insert", &head(&other)),
1142    };
1143    // MySQL `INSERT IGNORE`: a unique-key conflict skips the row, as `ON CONFLICT DO NOTHING`
1144    // (with no target) does. IGNORE also downgrades other errors, hence the allowance.
1145    let on_conflict = if ignore {
1146        if on_conflict.is_some() {
1147            return no(
1148                "insert_option",
1149                "IGNORE together with ON DUPLICATE KEY UPDATE",
1150            );
1151        }
1152        add_note(
1153            "insert_ignore",
1154            "INSERT IGNORE downgrades every error of the statement to a warning; ON CONFLICT DO NOTHING \
1155             skips unique-key conflicts only",
1156            Allowance::IgnoreCoversOnlyUniqueConflicts,
1157        );
1158        Some(OnConflict {
1159            target: vec![],
1160            action: ConflictAction::DoNothing,
1161        })
1162    } else {
1163        on_conflict
1164    };
1165    Ok(Statement::Insert(Insert {
1166        table,
1167        columns,
1168        source,
1169        on_conflict,
1170        returning: returning(ret)?,
1171    }))
1172}
1173
1174fn update(u: sp::Update) -> R<Statement> {
1175    let sp::Update {
1176        update_token: _,
1177        optimizer_hints,
1178        table,
1179        assignments: asg,
1180        from,
1181        selection,
1182        returning: ret,
1183        output,
1184        or,
1185        order_by,
1186        limit,
1187    } = u;
1188    if !optimizer_hints.is_empty()
1189        || output.is_some()
1190        || or.is_some()
1191        || !order_by.is_empty()
1192        || limit.is_some()
1193    {
1194        return no(
1195            "update_option",
1196            "UPDATE with hints/OUTPUT/OR/ORDER BY/LIMIT",
1197        );
1198    }
1199    let (table, alias) = simple_table(table)?;
1200    let from = match from {
1201        None => Vec::new(),
1202        Some(sp::UpdateTableFromKind::AfterSet(ts)) => ts
1203            .into_iter()
1204            .map(table_with_joins)
1205            .collect::<R<Vec<_>>>()?,
1206        Some(other) => return no("update_from_position", &head(&other)),
1207    };
1208    Ok(Statement::Update(Update {
1209        table,
1210        alias,
1211        set: assignments(asg)?,
1212        from,
1213        where_: selection.map(expr).transpose()?,
1214        returning: returning(ret)?,
1215    }))
1216}
1217
1218fn delete(d: sp::Delete) -> R<Statement> {
1219    let sp::Delete {
1220        delete_token: _,
1221        optimizer_hints,
1222        tables,
1223        from,
1224        using,
1225        selection,
1226        returning: ret,
1227        output,
1228        order_by,
1229        limit,
1230    } = d;
1231    if !optimizer_hints.is_empty()
1232        || !tables.is_empty()
1233        || output.is_some()
1234        || !order_by.is_empty()
1235        || limit.is_some()
1236    {
1237        return no(
1238            "delete_option",
1239            "DELETE with hints/multi-table/OUTPUT/ORDER BY/LIMIT",
1240        );
1241    }
1242    let ts = match from {
1243        sp::FromTable::WithFromKeyword(ts) | sp::FromTable::WithoutKeyword(ts) => ts,
1244    };
1245    if ts.len() != 1 {
1246        return no("delete_multi_target", "DELETE FROM a, b");
1247    }
1248    let (table, alias) = simple_table(ts.into_iter().next().unwrap())?;
1249    let using = using
1250        .unwrap_or_default()
1251        .into_iter()
1252        .map(table_with_joins)
1253        .collect::<R<Vec<_>>>()?;
1254    Ok(Statement::Delete(Delete {
1255        table,
1256        alias,
1257        using,
1258        where_: selection.map(expr).transpose()?,
1259        returning: returning(ret)?,
1260    }))
1261}
1262
1263fn boxed(e: sp::Expr) -> R<Box<Expr>> {
1264    Ok(Box::new(expr(e)?))
1265}
1266
1267fn expr(e: sp::Expr) -> R<Expr> {
1268    use sp::Expr as E;
1269    Ok(match e {
1270        E::Identifier(i) => Expr::Column(Name::simple(ident(&i))),
1271        E::CompoundIdentifier(parts) => Expr::Column(Name(parts.iter().map(ident).collect())),
1272        E::Value(v) => value(v.value)?,
1273        E::Nested(inner) => Expr::Nested(boxed(*inner)?),
1274        E::IsNull(x) => Expr::IsNull {
1275            expr: boxed(*x)?,
1276            negated: false,
1277        },
1278        E::IsNotNull(x) => Expr::IsNull {
1279            expr: boxed(*x)?,
1280            negated: true,
1281        },
1282        E::InList {
1283            expr: x,
1284            list,
1285            negated,
1286        } => Expr::InList {
1287            expr: boxed(*x)?,
1288            list: list.into_iter().map(expr).collect::<R<_>>()?,
1289            negated,
1290        },
1291        E::InSubquery {
1292            expr: x,
1293            subquery,
1294            negated,
1295        } => Expr::InSubquery {
1296            expr: boxed(*x)?,
1297            query: Box::new(query(*subquery)?),
1298            negated,
1299        },
1300        E::Between {
1301            expr: x,
1302            negated,
1303            low,
1304            high,
1305        } => Expr::Between {
1306            expr: boxed(*x)?,
1307            low: boxed(*low)?,
1308            high: boxed(*high)?,
1309            negated,
1310        },
1311        E::Like {
1312            negated,
1313            any,
1314            expr: x,
1315            pattern,
1316            escape_char,
1317        } => {
1318            if any {
1319                return no("like_option", "LIKE ANY");
1320            }
1321            let ci = rules().like_ci_default;
1322            if ci {
1323                add_note(
1324                    "like",
1325                    "the source LIKE is ASCII case-insensitive only while its connection setting is at the default",
1326                    Allowance::LikeCaseSensitivityIsConnectionSetting,
1327                );
1328            }
1329            if rules().name == "mysql" {
1330                add_note(
1331                    "like",
1332                    "the source LIKE follows the column collation; read as case-sensitive (a binary collation)",
1333                    Allowance::LikeCaseSensitivityIsConnectionSetting,
1334                );
1335            }
1336            like_expr(*x, *pattern, escape_char, negated, ci)?
1337        }
1338        E::ILike {
1339            negated,
1340            any,
1341            expr: x,
1342            pattern,
1343            escape_char,
1344        } => {
1345            if any {
1346                return no("like_option", "ILIKE ANY");
1347            }
1348            like_expr(*x, *pattern, escape_char, negated, true)?
1349        }
1350        E::UnaryOp { op, expr: x } => {
1351            let op = match op {
1352                sp::UnaryOperator::Not => UnaryOp::Not,
1353                sp::UnaryOperator::Minus => UnaryOp::Minus,
1354                sp::UnaryOperator::Plus => UnaryOp::Plus,
1355                other => return no("unary_operator", &head(&other)),
1356            };
1357            Expr::Unary {
1358                op,
1359                expr: boxed(*x)?,
1360            }
1361        }
1362        E::BinaryOp { left, op, right } => {
1363            use sp::BinaryOperator as B;
1364            let op = match op {
1365                B::Eq => BinaryOp::Eq,
1366                B::NotEq => BinaryOp::NotEq,
1367                B::Lt => BinaryOp::Lt,
1368                B::LtEq => BinaryOp::LtEq,
1369                B::Gt => BinaryOp::Gt,
1370                B::GtEq => BinaryOp::GtEq,
1371                B::And => BinaryOp::And,
1372                B::Or => BinaryOp::Or,
1373                B::Plus => BinaryOp::Plus,
1374                B::Minus => BinaryOp::Minus,
1375                B::Multiply => BinaryOp::Multiply,
1376                B::Divide => BinaryOp::Divide,
1377                B::Modulo => BinaryOp::Modulo,
1378                // MySQL's default sql_mode makes `||` a logical OR with OR's (lowest) precedence, but
1379                // sqlparser parses it as concatenation with concatenation's precedence, so the tree
1380                // is wrong either way: refuse instead of guessing.
1381                B::StringConcat if rules().name == "mysql" => {
1382                    return no(
1383                        "pipes_operator",
1384                        "MySQL `||` is logical OR (default sql_mode) but the parser gives it concatenation precedence; write OR, or CONCAT()",
1385                    );
1386                }
1387                B::StringConcat => BinaryOp::Concat,
1388                B::MyIntegerDivide => BinaryOp::IntDivide,
1389                B::Spaceship => BinaryOp::NullSafeEq,
1390                B::Arrow => BinaryOp::JsonGet,
1391                B::LongArrow => BinaryOp::JsonGetText,
1392                other => {
1393                    return no(
1394                        "binary_operator",
1395                        &format!(
1396                            "{} (JSON/array/regex/bitwise operators are not in the IR)",
1397                            head(&other)
1398                        ),
1399                    );
1400                }
1401            };
1402            if op == BinaryOp::Divide && rules().name == "mysql" {
1403                add_note(
1404                    "division",
1405                    "the source `/` is exact (DECIMAL) division; the target divides as double precision",
1406                    Allowance::DecimalDivisionAsFloat,
1407                );
1408                let l = Expr::Cast {
1409                    expr: boxed(*left)?,
1410                    ty: SqlType::Double,
1411                };
1412                return Ok(Expr::Binary {
1413                    op,
1414                    left: Box::new(l),
1415                    right: boxed(*right)?,
1416                });
1417            }
1418            // `ts +/- INTERVAL 'n unit'` is the same statement as `ts +/- make_interval(unit => n)`:
1419            // one IR form, so every target's interval arithmetic (and its refusal of months/years)
1420            // applies unchanged. `INTERVAL 'n unit' + ts` commutes for `+` only.
1421            match (op, *left, *right) {
1422                (BinaryOp::Plus | BinaryOp::Minus, l, E::Interval(iv)) => Expr::Binary {
1423                    op,
1424                    left: boxed(l)?,
1425                    right: Box::new(interval_call(iv)?),
1426                },
1427                (BinaryOp::Plus, E::Interval(iv), r) => Expr::Binary {
1428                    op,
1429                    left: boxed(r)?,
1430                    right: Box::new(interval_call(iv)?),
1431                },
1432                (op, l, r) => Expr::Binary {
1433                    op,
1434                    left: boxed(l)?,
1435                    right: boxed(r)?,
1436                },
1437            }
1438        }
1439        E::Case {
1440            case_token: _,
1441            end_token: _,
1442            operand,
1443            conditions,
1444            else_result,
1445        } => Expr::Case {
1446            operand: operand.map(|o| boxed(*o)).transpose()?,
1447            branches: conditions
1448                .into_iter()
1449                .map(|sp::CaseWhen { condition, result }| Ok((expr(condition)?, expr(result)?)))
1450                .collect::<R<_>>()?,
1451            else_: else_result.map(|o| boxed(*o)).transpose()?,
1452        },
1453        E::Cast {
1454            kind,
1455            expr: x,
1456            data_type,
1457            format,
1458        } => {
1459            if format.is_some() || !matches!(kind, sp::CastKind::Cast | sp::CastKind::DoubleColon) {
1460                return no("cast_form", "TRY_CAST / SAFE_CAST / CAST .. FORMAT");
1461            }
1462            Expr::Cast {
1463                expr: boxed(*x)?,
1464                ty: data_type_to_ir(&data_type)?,
1465            }
1466        }
1467        E::Trim {
1468            trim_where,
1469            trim_what,
1470            expr: x,
1471            trim_characters,
1472        } => {
1473            // `trim(chars FROM x)` / `trim(BOTH chars FROM x)`; the comma form is `trim_characters`.
1474            let side = match trim_where {
1475                None | Some(sp::TrimWhereField::Both) => TrimSide::Both,
1476                Some(sp::TrimWhereField::Leading) => TrimSide::Leading,
1477                Some(sp::TrimWhereField::Trailing) => TrimSide::Trailing,
1478            };
1479            let chars = match (trim_what, trim_characters) {
1480                (Some(w), None) => Some(boxed(*w)?),
1481                (None, None) => None,
1482                (None, Some(mut cs)) if cs.len() == 1 => Some(boxed(cs.remove(0))?),
1483                _ => return no("trim_form", "TRIM with several character arguments"),
1484            };
1485            Expr::Trim {
1486                expr: boxed(*x)?,
1487                side,
1488                chars,
1489            }
1490        }
1491        E::Substring {
1492            expr: x,
1493            substring_from,
1494            substring_for,
1495            special: _,
1496            shorthand: _,
1497        } => {
1498            if rules().name == "mysql" {
1499                // From position 1 and a non-negative length the engines agree; any other start
1500                // (0, negative, an expression) or length is the caller's stated assumption.
1501                let positive = |e: &E, min: u64| {
1502                    matches!(
1503                        e,
1504                        E::Value(v) if matches!(&v.value, sp::Value::Number(n, _)
1505                            if n.parse::<u64>().is_ok_and(|x| x >= min))
1506                    )
1507                };
1508                let start_ok = substring_from.as_deref().is_some_and(|f| positive(f, 1));
1509                let len_ok = substring_for.as_deref().is_none_or(|l| positive(l, 0));
1510                if !(start_ok && len_ok) {
1511                    add_note(
1512                        "fn:substr.start_position",
1513                        "MySQL SUBSTRING counts a start of 0 as empty and a negative start from the end; \
1514                         the target does not",
1515                        Allowance::SubstringStartAssumedPositive,
1516                    );
1517                }
1518            }
1519            // `substring(x FROM a FOR b)` and `substring(x, a, b)` are `substr(x, a, b)`.
1520            let mut args = vec![FunctionArg::Expr(expr(*x)?)];
1521            match (substring_from, substring_for) {
1522                (Some(f), Some(l)) => {
1523                    args.push(FunctionArg::Expr(expr(*f)?));
1524                    args.push(FunctionArg::Expr(expr(*l)?));
1525                }
1526                (Some(f), None) => args.push(FunctionArg::Expr(expr(*f)?)),
1527                (None, Some(l)) => {
1528                    // FOR without FROM starts at position 1.
1529                    args.push(FunctionArg::Expr(Expr::Literal(Literal::Number(
1530                        "1".into(),
1531                    ))));
1532                    args.push(FunctionArg::Expr(expr(*l)?));
1533                }
1534                (None, None) => return no("substring_form", "SUBSTRING without FROM or FOR"),
1535            }
1536            Expr::Function(Function {
1537                name: "substr".into(),
1538                args,
1539                distinct: false,
1540                star: false,
1541                filter: None,
1542                over: None,
1543                order_by: vec![],
1544            })
1545        }
1546        E::AnyOp {
1547            left,
1548            compare_op,
1549            right,
1550            is_some: _,
1551        } => {
1552            if !matches!(compare_op, sp::BinaryOperator::Eq) {
1553                return no("any_operator", "only `= ANY(..)` is in the IR");
1554            }
1555            quantified(*left, *right, false)?
1556        }
1557        E::AllOp {
1558            left,
1559            compare_op,
1560            right,
1561        } => {
1562            if !matches!(compare_op, sp::BinaryOperator::NotEq) {
1563                return no("all_operator", "only `<> ALL(..)` is in the IR");
1564            }
1565            quantified(*left, *right, true)?
1566        }
1567        E::Exists { subquery, negated } => Expr::Exists {
1568            query: Box::new(query(*subquery)?),
1569            negated,
1570        },
1571        E::Subquery(q) => Expr::Subquery(Box::new(query(*q)?)),
1572        E::Extract {
1573            field,
1574            syntax: _,
1575            expr: x,
1576        } => Expr::Extract {
1577            field: field.to_string().to_ascii_lowercase(),
1578            expr: boxed(*x)?,
1579        },
1580        E::Array(arr) => Expr::ArrayLiteral(arr.elem.into_iter().map(expr).collect::<R<Vec<_>>>()?),
1581        E::AtTimeZone {
1582            timestamp,
1583            time_zone,
1584        } => Expr::AtTimeZone {
1585            expr: boxed(*timestamp)?,
1586            zone: boxed(*time_zone)?,
1587        },
1588        E::Function(f) => function(f)?,
1589        other => return no("expression", &head(&other)),
1590    })
1591}
1592
1593/// `x = ANY(rhs)` / `x <> ALL(rhs)`: an array parameter, an array literal, or a subquery.
1594fn quantified(left: sp::Expr, right: sp::Expr, negated: bool) -> R<Expr> {
1595    let lhs = boxed(left)?;
1596    // `$1::int[]` is only a type hint on the parameter.
1597    let mut right = right;
1598    if let sp::Expr::Cast {
1599        kind: sp::CastKind::Cast | sp::CastKind::DoubleColon,
1600        expr: inner,
1601        data_type: sp::DataType::Array(_),
1602        format: None,
1603    } = right.clone()
1604    {
1605        right = *inner;
1606    }
1607    match right {
1608        sp::Expr::Value(v) => match v.value {
1609            sp::Value::Placeholder(_) => match value(v.value)? {
1610                Expr::Param(n) => Ok(Expr::AnyParam {
1611                    expr: lhs,
1612                    param: n,
1613                    negated,
1614                }),
1615                _ => no("array_param", "expected a numbered parameter"),
1616            },
1617            other => no("array_operand", &head(&other)),
1618        },
1619        sp::Expr::Array(sp::Array { elem, named: _ }) => Ok(Expr::InList {
1620            expr: lhs,
1621            list: elem.into_iter().map(expr).collect::<R<Vec<_>>>()?,
1622            negated,
1623        }),
1624        sp::Expr::Subquery(q) => Ok(Expr::InSubquery {
1625            expr: lhs,
1626            query: Box::new(query(*q)?),
1627            negated,
1628        }),
1629        other => no("array_operand", &head(&other)),
1630    }
1631}
1632
1633/// MySQL's `VALUES(col)` inside `ON DUPLICATE KEY UPDATE` refers to the row proposed for insertion.
1634fn excluded_refs(e: Expr) -> Expr {
1635    match e {
1636        Expr::Function(f) if f.name == "values" && f.args.len() == 1 => {
1637            match f.args.into_iter().next() {
1638                Some(FunctionArg::Expr(Expr::Column(n))) => {
1639                    Expr::Column(Name(vec!["excluded".to_string(), n.last().to_string()]))
1640                }
1641                other => Expr::Function(Function {
1642                    name: "values".into(),
1643                    args: other.into_iter().collect(),
1644                    distinct: false,
1645                    star: false,
1646                    filter: None,
1647                    over: None,
1648                    order_by: vec![],
1649                }),
1650            }
1651        }
1652        Expr::Binary { op, left, right } => Expr::Binary {
1653            op,
1654            left: Box::new(excluded_refs(*left)),
1655            right: Box::new(excluded_refs(*right)),
1656        },
1657        Expr::Nested(x) => Expr::Nested(Box::new(excluded_refs(*x))),
1658        Expr::Unary { op, expr } => Expr::Unary {
1659            op,
1660            expr: Box::new(excluded_refs(*expr)),
1661        },
1662        other => other,
1663    }
1664}
1665
1666fn like_expr(
1667    x: sp::Expr,
1668    pattern: sp::Expr,
1669    escape_char: Option<Box<sp::Expr>>,
1670    negated: bool,
1671    case_insensitive: bool,
1672) -> R<Expr> {
1673    let escape = match escape_char {
1674        Some(e) => Some(boxed(*e)?),
1675        None if rules().like_default_escape => {
1676            Some(Box::new(Expr::Literal(Literal::String("\\".into()))))
1677        }
1678        None => None,
1679    };
1680    Ok(Expr::Like {
1681        expr: boxed(x)?,
1682        pattern: boxed(pattern)?,
1683        negated,
1684        case_insensitive,
1685        escape,
1686    })
1687}
1688
1689fn value(v: sp::Value) -> R<Expr> {
1690    Ok(match v {
1691        sp::Value::Number(n, long) => {
1692            if long {
1693                return no("numeric_suffix", "L suffix");
1694            }
1695            Expr::Literal(Literal::Number(n))
1696        }
1697        sp::Value::SingleQuotedString(s) => Expr::Literal(Literal::String(s)),
1698        // The tokenizer has already resolved the backslash escapes of `E'..'`.
1699        sp::Value::EscapedStringLiteral(s) => Expr::Literal(Literal::String(s)),
1700        sp::Value::Boolean(b) if rules().name == "mysql" => {
1701            // MySQL TRUE/FALSE are the integers 1 and 0.
1702            Expr::Literal(Literal::Number(if b { "1" } else { "0" }.into()))
1703        }
1704        sp::Value::Boolean(b) => Expr::Literal(Literal::Bool(b)),
1705        sp::Value::Null => Expr::Literal(Literal::Null),
1706        sp::Value::Placeholder(p) => {
1707            let bad = || ReadError::NotInIr {
1708                construct: "placeholder".into(),
1709                detail: format!(
1710                    "`{p}` is not a placeholder form of dialect `{}`",
1711                    rules().name
1712                ),
1713            };
1714            if rules().question_placeholders {
1715                if p == "?" {
1716                    Expr::Param(bump_param(None, None))
1717                } else if let Some(d) = p.strip_prefix('?') {
1718                    let n = d.parse::<u32>().ok().filter(|n| *n >= 1).ok_or_else(bad)?;
1719                    Expr::Param(bump_param(Some(n), None))
1720                } else if let Some(nm) = p.strip_prefix([':', '@', '$']) {
1721                    Expr::Param(bump_param(None, Some(nm)))
1722                } else {
1723                    return Err(bad());
1724                }
1725            } else {
1726                let n = p
1727                    .strip_prefix('$')
1728                    .and_then(|d| d.parse::<u32>().ok())
1729                    .filter(|n| *n >= 1)
1730                    .ok_or_else(bad)?;
1731                Expr::Param(n)
1732            }
1733        }
1734        other => return no("literal", &head(&other)),
1735    })
1736}
1737
1738fn function(f: sp::Function) -> R<Expr> {
1739    let sp::Function {
1740        name,
1741        uses_odbc_syntax,
1742        parameters,
1743        args,
1744        within_group,
1745        filter,
1746        null_treatment,
1747        over,
1748    } = f;
1749    if uses_odbc_syntax
1750        || !matches!(parameters, sp::FunctionArguments::None)
1751        || !within_group.is_empty()
1752        || null_treatment.is_some()
1753    {
1754        return no(
1755            "function_option",
1756            "ODBC syntax/parameters/WITHIN GROUP/FILTER/IGNORE NULLS",
1757        );
1758    }
1759    let over = match over {
1760        None => None,
1761        Some(sp::WindowType::NamedWindow(_)) => {
1762            return no(
1763                "named_window",
1764                "OVER w (a WINDOW clause) is not in the IR yet",
1765            );
1766        }
1767        Some(sp::WindowType::WindowSpec(spec)) => Some(window_spec(spec)?),
1768    };
1769    let filter = filter.map(|f| expr(*f).map(Box::new)).transpose()?;
1770    let name = object_name(&name)?;
1771    let base = name.last().to_string();
1772    if name.0.len() != 1 {
1773        return no("qualified_function", &name.0.join("."));
1774    }
1775    let args = match args {
1776        // `CURRENT_TIMESTAMP` is a function without parentheses.
1777        sp::FunctionArguments::None => {
1778            if base == "current_timestamp" {
1779                if rules().name == "sqlite" {
1780                    add_note(
1781                        "fn:now",
1782                        "the source CURRENT_TIMESTAMP is text 'YYYY-MM-DD HH:MM:SS' (UTC), not a timestamptz",
1783                        Allowance::NowAsText,
1784                    );
1785                }
1786                return Ok(Expr::Function(Function {
1787                    name: "now".to_string(),
1788                    args: vec![],
1789                    distinct: false,
1790                    star: false,
1791                    filter: None,
1792                    over: None,
1793                    order_by: vec![],
1794                }));
1795            }
1796            // The session identity keywords are zero-argument functions.
1797            if matches!(
1798                base.as_str(),
1799                "current_user" | "session_user" | "current_role"
1800            ) {
1801                return Ok(Expr::Function(Function {
1802                    name: base,
1803                    args: vec![],
1804                    distinct: false,
1805                    star: false,
1806                    filter: None,
1807                    over: None,
1808                    order_by: vec![],
1809                }));
1810            }
1811            // `CURRENT_DATE`: a date in the session time zone (PostgreSQL, MySQL), text of the UTC
1812            // date on SQLite.
1813            if base == "current_date" {
1814                if rules().name == "sqlite" {
1815                    add_note(
1816                        "fn:current_date",
1817                        "the source CURRENT_DATE is text 'YYYY-MM-DD' (the UTC date), not a date",
1818                        Allowance::DateAsText,
1819                    );
1820                }
1821                return Ok(Expr::Function(Function {
1822                    name: "current_date".to_string(),
1823                    args: vec![],
1824                    distinct: false,
1825                    star: false,
1826                    filter: None,
1827                    over: None,
1828                    order_by: vec![],
1829                }));
1830            }
1831            return no("bare_function", &format!("`{base}` without parentheses"));
1832        }
1833        sp::FunctionArguments::Subquery(q) if base == "array" => {
1834            return Ok(Expr::ArraySubquery(Box::new(query(*q)?)));
1835        }
1836        sp::FunctionArguments::Subquery(_) => {
1837            return no("function_subquery", "function over a subquery");
1838        }
1839        sp::FunctionArguments::List(l) => l,
1840    };
1841    let sp::FunctionArgumentList {
1842        duplicate_treatment,
1843        args,
1844        clauses,
1845    } = args;
1846    let mut agg_order = Vec::new();
1847    let mut separator: Option<String> = None;
1848    for cl in clauses {
1849        match cl {
1850            sp::FunctionArgumentClause::OrderBy(items) => {
1851                for it in items {
1852                    agg_order.push(order_item(it)?);
1853                }
1854            }
1855            sp::FunctionArgumentClause::Separator(v) => match v.value {
1856                sp::Value::SingleQuotedString(s) => separator = Some(s),
1857                other => return no("separator", &head(&other)),
1858            },
1859            other => {
1860                return no(
1861                    "function_clause",
1862                    &format!("{} inside a function call", head(&other)),
1863                );
1864            }
1865        }
1866    }
1867    let distinct = match duplicate_treatment {
1868        None | Some(sp::DuplicateTreatment::All) => false,
1869        Some(sp::DuplicateTreatment::Distinct) => true,
1870    };
1871    let mut out = Vec::new();
1872    let mut star = false;
1873    for a in args {
1874        match a {
1875            sp::FunctionArg::Unnamed(sp::FunctionArgExpr::Expr(e)) => {
1876                out.push(FunctionArg::Expr(expr(e)?))
1877            }
1878            sp::FunctionArg::Unnamed(sp::FunctionArgExpr::Wildcard) => star = true,
1879            sp::FunctionArg::Named {
1880                name,
1881                arg: sp::FunctionArgExpr::Expr(e),
1882                operator: sp::FunctionArgOperator::RightArrow,
1883            } => out.push(FunctionArg::Named {
1884                name: ident(&name),
1885                value: expr(e)?,
1886            }),
1887            // sqlparser produces `ExprNamed` when the name is parsed as an expression.
1888            sp::FunctionArg::ExprNamed {
1889                name: sp::Expr::Identifier(name),
1890                arg: sp::FunctionArgExpr::Expr(e),
1891                operator: sp::FunctionArgOperator::RightArrow,
1892            } => out.push(FunctionArg::Named {
1893                name: ident(&name),
1894                value: expr(e)?,
1895            }),
1896            other => return no("function_argument", &head(&other)),
1897        }
1898    }
1899    let mut name = base;
1900    if name == "current_timestamp" {
1901        name = "now".to_string();
1902    }
1903    if rules().name == "mysql" {
1904        match name.as_str() {
1905            "ifnull" => name = "coalesce".to_string(),
1906            "group_concat" => {
1907                name = "string_agg".to_string();
1908                out.push(FunctionArg::Expr(Expr::Literal(Literal::String(
1909                    separator.clone().unwrap_or_else(|| ",".to_string()),
1910                ))));
1911                add_note(
1912                    "fn:string_agg",
1913                    "the source GROUP_CONCAT truncates silently at group_concat_max_len",
1914                    Allowance::AggregateStringTruncation,
1915                );
1916            }
1917            // MySQL LENGTH() counts bytes.
1918            "length" => name = "octet_length".to_string(),
1919            "concat" if out.len() >= 2 => {
1920                let mut it = out.into_iter().map(|a| match a {
1921                    FunctionArg::Expr(e) => e,
1922                    FunctionArg::Named { value, .. } => value,
1923                });
1924                let first = it.next().expect("two arguments");
1925                let chain = it.fold(first, |l, r| Expr::Binary {
1926                    op: BinaryOp::Concat,
1927                    left: Box::new(l),
1928                    right: Box::new(r),
1929                });
1930                return Ok(chain);
1931            }
1932            "if" if out.len() == 3 => {
1933                let mut it = out.into_iter().map(|a| match a {
1934                    FunctionArg::Expr(e) => e,
1935                    FunctionArg::Named { value, .. } => value,
1936                });
1937                let (c, t, f) = (it.next().unwrap(), it.next().unwrap(), it.next().unwrap());
1938                return Ok(Expr::Case {
1939                    operand: None,
1940                    branches: vec![(c, t)],
1941                    else_: Some(Box::new(f)),
1942                });
1943            }
1944            _ => {}
1945        }
1946    }
1947    if rules().name == "sqlite" {
1948        match name.as_str() {
1949            "ifnull" => name = "coalesce".to_string(),
1950            "group_concat" => {
1951                name = "string_agg".to_string();
1952                if out.len() == 1 {
1953                    out.push(FunctionArg::Expr(Expr::Literal(Literal::String(
1954                        ",".into(),
1955                    ))));
1956                }
1957            }
1958            _ => {}
1959        }
1960    }
1961    Ok(Expr::Function(Function {
1962        name,
1963        args: out,
1964        distinct,
1965        star,
1966        filter,
1967        over,
1968        order_by: agg_order,
1969    }))
1970}
1971
1972/// `INTERVAL '3 days'` / `INTERVAL 3 DAY` as `make_interval(days => 3)`. Only a single `<number> <unit>`
1973/// is represented; compound intervals (`'1 day 2 hours'`), ranges (`DAY TO SECOND`) and unit-less
1974/// strings are not in the IR. Months and years are read (and then refused by the targets, whose
1975/// month arithmetic differs), not guessed.
1976fn interval_call(iv: sp::Interval) -> R<Expr> {
1977    let sp::Interval {
1978        value,
1979        leading_field,
1980        leading_precision,
1981        last_field,
1982        fractional_seconds_precision,
1983    } = iv;
1984    if leading_precision.is_some() || last_field.is_some() || fractional_seconds_precision.is_some()
1985    {
1986        return no(
1987            "interval",
1988            "interval ranges and precisions are not in the IR",
1989        );
1990    }
1991    let (amount, unit) = match (*value, leading_field) {
1992        // MySQL `INTERVAL 3 DAY`
1993        (sp::Expr::Value(v), Some(field)) => (
1994            match v.value {
1995                sp::Value::Number(n, _) => n,
1996                other => return no("interval", &head(&other)),
1997            },
1998            field.to_string(),
1999        ),
2000        // PostgreSQL / standard `INTERVAL '3 days'`
2001        (sp::Expr::Value(v), None) => match v.value {
2002            sp::Value::SingleQuotedString(s) => {
2003                let mut it = s.split_whitespace();
2004                match (it.next(), it.next(), it.next()) {
2005                    (Some(a), Some(u), None) => (a.to_string(), u.to_string()),
2006                    _ => {
2007                        return no(
2008                            "interval",
2009                            "only a single `<number> <unit>` interval is in the IR",
2010                        );
2011                    }
2012                }
2013            }
2014            other => return no("interval", &head(&other)),
2015        },
2016        (other, _) => return no("interval", &head(&other)),
2017    };
2018    let numeric = !amount.is_empty()
2019        && amount
2020            .trim_start_matches('-')
2021            .chars()
2022            .all(|c| c.is_ascii_digit() || c == '.')
2023        && amount
2024            .trim_start_matches('-')
2025            .chars()
2026            .any(|c| c.is_ascii_digit());
2027    if !numeric {
2028        return no("interval", "the interval amount is not a plain number");
2029    }
2030    let unit = match unit.to_ascii_lowercase().trim_end_matches('s') {
2031        "second" | "sec" => "secs",
2032        "minute" | "min" => "mins",
2033        "hour" => "hours",
2034        "day" => "days",
2035        "week" => "weeks",
2036        "month" | "mon" => "months",
2037        "year" => "years",
2038        other => return no("interval", &format!("unit `{other}` is not in the IR")),
2039    };
2040    Ok(Expr::Function(Function {
2041        name: "make_interval".to_string(),
2042        args: vec![FunctionArg::Named {
2043            name: unit.to_string(),
2044            value: Expr::Literal(Literal::Number(amount)),
2045        }],
2046        distinct: false,
2047        star: false,
2048        filter: None,
2049        over: None,
2050        order_by: vec![],
2051    }))
2052}
2053
2054fn window_spec(w: sp::WindowSpec) -> R<Window> {
2055    let sp::WindowSpec {
2056        window_name,
2057        partition_by,
2058        order_by,
2059        window_frame,
2060    } = w;
2061    if window_name.is_some() {
2062        return no("named_window", "OVER (w ..) refinement of a named window");
2063    }
2064    let frame = match window_frame {
2065        None => None,
2066        Some(sp::WindowFrame {
2067            units,
2068            start_bound,
2069            end_bound,
2070        }) => {
2071            let bound = |b: sp::WindowFrameBound| -> R<FrameBound> {
2072                Ok(match b {
2073                    sp::WindowFrameBound::CurrentRow => FrameBound::CurrentRow,
2074                    sp::WindowFrameBound::Preceding(e) => {
2075                        FrameBound::Preceding(e.map(|x| expr(*x).map(Box::new)).transpose()?)
2076                    }
2077                    sp::WindowFrameBound::Following(e) => {
2078                        FrameBound::Following(e.map(|x| expr(*x).map(Box::new)).transpose()?)
2079                    }
2080                })
2081            };
2082            Some(Frame {
2083                units: match units {
2084                    sp::WindowFrameUnits::Rows => FrameUnits::Rows,
2085                    sp::WindowFrameUnits::Range => FrameUnits::Range,
2086                    sp::WindowFrameUnits::Groups => FrameUnits::Groups,
2087                },
2088                start: bound(start_bound)?,
2089                end: end_bound.map(bound).transpose()?,
2090            })
2091        }
2092    };
2093    Ok(Window {
2094        partition_by: partition_by.into_iter().map(expr).collect::<R<_>>()?,
2095        order_by: order_by.into_iter().map(order_item).collect::<R<_>>()?,
2096        frame,
2097    })
2098}
2099
2100fn data_type_to_ir(t: &sp::DataType) -> R<SqlType> {
2101    use sp::DataType as D;
2102    Ok(match t {
2103        D::SmallInt(_) | D::Int2(_) => SqlType::SmallInt,
2104        D::Int(_) | D::Integer(_) | D::Int4(_) => SqlType::Integer,
2105        D::BigInt(_) | D::Int8(_) => SqlType::BigInt,
2106        D::Real | D::Float4 => SqlType::Real,
2107        D::Double(_) | D::DoublePrecision | D::Float8 => SqlType::Double,
2108        D::Numeric(info) | D::Decimal(info) => match info {
2109            sp::ExactNumberInfo::None => SqlType::Numeric {
2110                precision: None,
2111                scale: None,
2112            },
2113            sp::ExactNumberInfo::Precision(p) => SqlType::Numeric {
2114                precision: Some(*p as u32),
2115                scale: None,
2116            },
2117            sp::ExactNumberInfo::PrecisionAndScale(p, s) => SqlType::Numeric {
2118                precision: Some(*p as u32),
2119                scale: Some(*s as u32),
2120            },
2121        },
2122        D::Text => SqlType::Text,
2123        D::Signed | D::SignedInteger | D::Unsigned | D::UnsignedInteger => SqlType::BigInt,
2124        D::Char(None) | D::Character(None) => SqlType::Text,
2125        D::Datetime(None) => SqlType::Timestamp { with_tz: false },
2126        D::Varchar(None) | D::CharVarying(None) => SqlType::Varchar(None),
2127        D::Varchar(Some(sp::CharacterLength::IntegerLength { length, unit: None }))
2128        | D::CharVarying(Some(sp::CharacterLength::IntegerLength { length, unit: None })) => {
2129            SqlType::Varchar(Some(*length as u32))
2130        }
2131        D::Boolean | D::Bool => SqlType::Boolean,
2132        D::Date => SqlType::Date,
2133        D::Timestamp(None, tz) => SqlType::Timestamp {
2134            with_tz: match tz {
2135                sp::TimezoneInfo::None | sp::TimezoneInfo::WithoutTimeZone => false,
2136                sp::TimezoneInfo::WithTimeZone | sp::TimezoneInfo::Tz => true,
2137            },
2138        },
2139        D::Uuid => SqlType::Uuid,
2140        D::Bytea => SqlType::Bytes,
2141        D::JSON => SqlType::Json,
2142        D::JSONB => SqlType::Jsonb,
2143        other => return no("data_type", &format!("{other}")),
2144    })
2145}