Skip to main content

rich_ext/schema/
sql.rs

1//! SQL DDL in the format-neutral model: a small `CREATE TABLE` subset.
2//!
3//! [`parse`] reads a file of statements into a [`Schema`] of tables, one
4//! per `CREATE TABLE`, each column a [`Field`] with its type as written
5//! (`VARCHAR(20)`, kept as the [native type](Field::native_type)) and the
6//! model's type for it (`string`). It reads:
7//!
8//! - column names and types, quoted (`"x"`, `` `x` ``, `[x]`) or not, with
9//!   their parameters (`NUMERIC(10, 2)`), several words
10//!   (`DOUBLE PRECISION`, `TIMESTAMP WITH TIME ZONE`) and arrays (`INT[]`);
11//! - `NOT NULL` and `NULL`, `DEFAULT` (any expression, kept as written),
12//!   `PRIMARY KEY`, `UNIQUE` and `REFERENCES table (column)`, on a column
13//!   or, for one or several columns, on the table (`PRIMARY KEY (a, b)`,
14//!   `UNIQUE (a, b)`, `FOREIGN KEY (a) REFERENCES t (x)`), named with
15//!   `CONSTRAINT name` or not, and MySQL's `ENUM('a', 'b')` and `COMMENT`;
16//! - `--` and `/* … */` comments, PostgreSQL's dollar-quoted strings
17//!   (`$$ … $$`, a function body's), and any number of statements, each
18//!   ended by `;` or, in a script that leaves it out, by a `GO` line or the
19//!   `CREATE` that starts the next.
20//!
21//! `CHECK` constraints, indexes, generated columns and other clauses are
22//! skipped, as is every statement but `CREATE TABLE` (`CREATE INDEX`,
23//! `ALTER TABLE`, `INSERT`, …); each skip is a [`Note`] with its line, so
24//! nothing is dropped silently. Text that cannot be read (an unclosed
25//! string, comment or parenthesis, a `CREATE TABLE` without a column list)
26//! is a [`SqlError`] with its line. Input is bounded by [`MAX_INPUT`],
27//! [`MAX_STATEMENTS`], [`MAX_TOKENS`], [`MAX_COLUMNS`] and [`MAX_NESTING`],
28//! statements are read one at a time, and the reader never panics.
29//!
30//! ```
31//! use rich_ext::schema::{sql, DataType};
32//!
33//! let ddl = "
34//!     -- The people.
35//!     CREATE TABLE users (
36//!         id    BIGINT PRIMARY KEY,
37//!         email VARCHAR(255) NOT NULL UNIQUE,
38//!         team  INT REFERENCES teams (id),
39//!         CHECK (email <> '')
40//!     );
41//!     CREATE INDEX users_email ON users (email);
42//! ";
43//! let parsed = sql::parse(ddl).unwrap();
44//! let users = parsed.schema.table("users").unwrap();
45//! let email = users.field("email").unwrap();
46//! assert_eq!(email.native_type(), Some("VARCHAR(255)"));
47//! assert_eq!(email.data_type(), &DataType::String);
48//! assert!(email.is_required() && email.is_unique());
49//! assert_eq!(users.primary_key(), ["id"]);
50//! assert_eq!(users.foreign_keys()[0].to_string(), "team → teams.id");
51//! let notes: Vec<String> = parsed.notes.iter().map(ToString::to_string).collect();
52//! assert_eq!(
53//!     notes,
54//!     [
55//!         "line 7: users: CHECK constraint skipped",
56//!         "line 9: CREATE INDEX statement skipped",
57//!     ]
58//! );
59//! ```
60
61use std::collections::HashMap;
62use std::fmt;
63
64use super::model::{Constraint, DataType, Field, ForeignKey, Literal, Schema};
65
66/// The most bytes of DDL read.
67pub const MAX_INPUT: usize = 16 * 1024 * 1024;
68
69/// The most statements read.
70pub const MAX_STATEMENTS: usize = 100_000;
71
72/// The most columns (and table constraints) in one table, and the most
73/// columns one key names.
74pub const MAX_COLUMNS: usize = 4_096;
75
76/// The deepest parentheses nest.
77pub const MAX_NESTING: usize = 64;
78
79/// The most tokens (words, names, values, punctuation) in one statement.
80/// Only a `CREATE TABLE` statement's are kept, one statement at a time.
81pub const MAX_TOKENS: usize = 1_000_000;
82
83/// The most notes kept; past this they are counted in a last one.
84pub const MAX_NOTES: usize = 1_000;
85
86/// Why DDL could not be read, and on which line.
87#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct SqlError {
89    line: usize,
90    message: String,
91}
92
93impl SqlError {
94    fn new(line: usize, message: impl Into<String>) -> Self {
95        SqlError {
96            line,
97            message: message.into(),
98        }
99    }
100
101    /// The line (from 1).
102    pub fn line(&self) -> usize {
103        self.line
104    }
105
106    /// What was wrong.
107    pub fn message(&self) -> &str {
108        &self.message
109    }
110}
111
112impl fmt::Display for SqlError {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "line {}: {}", self.line, self.message)
115    }
116}
117
118impl std::error::Error for SqlError {}
119
120/// Something skipped, and on which line.
121#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct Note {
123    /// The line (from 1).
124    pub line: usize,
125    /// What was skipped.
126    pub message: String,
127}
128
129impl fmt::Display for Note {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        write!(f, "line {}: {}", self.line, self.message)
132    }
133}
134
135/// DDL read into the model: a schema of tables, and what was skipped.
136#[derive(Clone, Debug, PartialEq, Eq)]
137#[non_exhaustive]
138pub struct Parsed {
139    /// One table per `CREATE TABLE`, in order.
140    pub schema: Schema,
141    /// What was skipped, in order.
142    pub notes: Vec<Note>,
143}
144
145/// Read DDL. See the [module docs](self).
146pub fn parse(text: &str) -> Result<Parsed, SqlError> {
147    if text.len() > MAX_INPUT {
148        return Err(SqlError::new(
149            1,
150            format!(
151                "the DDL is {} bytes, past the limit of {MAX_INPUT}",
152                text.len()
153            ),
154        ));
155    }
156    check(text)?;
157    let mut reader = Reader {
158        schema: Schema::default(),
159        index: HashMap::new(),
160        notes: Vec::new(),
161        dropped: 0,
162    };
163    let mut statements = Statements::new(text);
164    let mut count = 0;
165    while let Some(statement) = statements.next()? {
166        count += 1;
167        if count > MAX_STATEMENTS {
168            return Err(SqlError::new(
169                statement[0].line,
170                format!("more than {MAX_STATEMENTS} statements"),
171            ));
172        }
173        reader.statement(&statement)?;
174    }
175    if reader.dropped > 0 {
176        let line = statements.last_line;
177        reader.notes.push(Note {
178            line,
179            message: format!("… and {} more notes", reader.dropped),
180        });
181    }
182    Ok(Parsed {
183        schema: reader.schema,
184        notes: reader.notes,
185    })
186}
187
188// --------------------------------------------------------------- tokens
189
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191enum Kind {
192    /// A bare word: a keyword or an unquoted name.
193    Word,
194    /// A quoted name.
195    Quoted,
196    /// A string literal, kept with its quotes.
197    Str,
198    /// A number.
199    Number,
200    /// Punctuation or an operator.
201    Symbol,
202}
203
204#[derive(Clone, Debug)]
205struct Token {
206    kind: Kind,
207    /// As written.
208    text: String,
209    /// A name's value: unquoted, escapes undone.
210    value: String,
211    line: usize,
212}
213
214impl Token {
215    /// Whether this is the keyword `word` (in any case).
216    fn is(&self, word: &str) -> bool {
217        self.kind == Kind::Word && self.text.eq_ignore_ascii_case(word)
218    }
219
220    fn is_symbol(&self, symbol: &str) -> bool {
221        self.kind == Kind::Symbol && self.text == symbol
222    }
223
224    fn is_name(&self) -> bool {
225        matches!(self.kind, Kind::Word | Kind::Quoted)
226    }
227}
228
229/// Reads the text a token at a time.
230struct Lexer {
231    chars: Vec<char>,
232    i: usize,
233    line: usize,
234}
235
236impl Lexer {
237    fn new(text: &str) -> Self {
238        Lexer {
239            chars: text.chars().collect(),
240            i: 0,
241            line: 1,
242        }
243    }
244
245    /// The next token, or `None` at the end of the text.
246    fn next(&mut self) -> Result<Option<Token>, SqlError> {
247        let chars = &self.chars;
248        let (mut i, mut line) = (self.i, self.line);
249        let take = |from: usize, to: usize| -> String { chars[from..to].iter().collect() };
250        let token = loop {
251            let Some(&c) = chars.get(i) else {
252                break None;
253            };
254            let next = chars.get(i + 1).copied();
255            if c == '\n' {
256                line += 1;
257                i += 1;
258            } else if c.is_whitespace() {
259                i += 1;
260            } else if c == '-' && next == Some('-') {
261                while i < chars.len() && chars[i] != '\n' {
262                    i += 1;
263                }
264            } else if c == '/' && next == Some('*') {
265                let start = line;
266                i += 2;
267                loop {
268                    match chars.get(i) {
269                        None => return Err(SqlError::new(start, "a /* comment is not closed")),
270                        Some('*') if chars.get(i + 1) == Some(&'/') => {
271                            i += 2;
272                            break;
273                        }
274                        Some('\n') => line += 1,
275                        _ => {}
276                    }
277                    i += 1;
278                }
279            } else if c == '[' && next.is_some_and(|n| n == ']' || n.is_ascii_digit()) {
280                // An array type's brackets (`INT[]`, `INT[3]`), not a name.
281                let len = if next == Some(']') { 2 } else { 1 };
282                let symbol = take(i, i + len);
283                i += len;
284                break Some(Token {
285                    kind: Kind::Symbol,
286                    value: symbol.clone(),
287                    text: symbol,
288                    line,
289                });
290            } else if matches!(c, '\'' | '"' | '`' | '[') {
291                let close = if c == '[' { ']' } else { c };
292                let start = (i, line);
293                let mut value = String::new();
294                i += 1;
295                loop {
296                    match chars.get(i) {
297                        None => {
298                            let what = if c == '\'' { "string" } else { "quoted name" };
299                            return Err(SqlError::new(start.1, format!("a {what} is not closed")));
300                        }
301                        Some(&ch) if ch == close => {
302                            // A doubled quote is one quote.
303                            if close != ']' && chars.get(i + 1) == Some(&close) {
304                                value.push(ch);
305                                i += 2;
306                                continue;
307                            }
308                            i += 1;
309                            break;
310                        }
311                        Some(&ch) => {
312                            if ch == '\n' {
313                                line += 1;
314                            }
315                            value.push(ch);
316                            i += 1;
317                        }
318                    }
319                }
320                let kind = if c == '\'' { Kind::Str } else { Kind::Quoted };
321                break Some(Token {
322                    kind,
323                    text: take(start.0, i),
324                    value,
325                    line: start.1,
326                });
327            } else if let Some(len) = dollar_tag(chars, i) {
328                // PostgreSQL's dollar-quoted string (`$$ … $$`, `$body$ …
329                // $body$`), a function body most often: its `;` and quotes
330                // are its own.
331                let start = (i, line);
332                let tag = &chars[i..i + len];
333                i += len;
334                loop {
335                    if i >= chars.len() {
336                        return Err(SqlError::new(start.1, "a string is not closed"));
337                    }
338                    if chars[i..].starts_with(tag) {
339                        i += len;
340                        break;
341                    }
342                    if chars[i] == '\n' {
343                        line += 1;
344                    }
345                    i += 1;
346                }
347                break Some(Token {
348                    kind: Kind::Str,
349                    text: take(start.0, i),
350                    value: take(start.0 + len, i - len),
351                    line: start.1,
352                });
353            } else if c.is_alphabetic() || c == '_' {
354                let start = i;
355                while i < chars.len()
356                    && (chars[i].is_alphanumeric() || matches!(chars[i], '_' | '$'))
357                {
358                    i += 1;
359                }
360                let word = take(start, i);
361                break Some(Token {
362                    kind: Kind::Word,
363                    value: word.clone(),
364                    text: word,
365                    line,
366                });
367            } else if c.is_ascii_digit() || (c == '.' && next.is_some_and(|n| n.is_ascii_digit())) {
368                let start = i;
369                while i < chars.len() {
370                    let ch = chars[i];
371                    let exponent_sign =
372                        matches!(ch, '+' | '-') && i > start && matches!(chars[i - 1], 'e' | 'E');
373                    if ch.is_ascii_digit() || matches!(ch, '.' | 'e' | 'E') || exponent_sign {
374                        i += 1;
375                    } else {
376                        break;
377                    }
378                }
379                let number = take(start, i);
380                break Some(Token {
381                    kind: Kind::Number,
382                    value: number.clone(),
383                    text: number,
384                    line,
385                });
386            } else {
387                // `::` (a cast) is one symbol; anything else is one character.
388                let len = if c == ':' && next == Some(':') { 2 } else { 1 };
389                let symbol = take(i, i + len);
390                i += len;
391                break Some(Token {
392                    kind: Kind::Symbol,
393                    value: symbol.clone(),
394                    text: symbol,
395                    line,
396                });
397            }
398        };
399        self.i = i;
400        self.line = line;
401        Ok(token)
402    }
403}
404
405/// The length of the dollar quote (`$$`, `$tag$`) at `i`, if one starts
406/// there; `$1` is a parameter.
407fn dollar_tag(chars: &[char], i: usize) -> Option<usize> {
408    if chars.get(i) != Some(&'$') {
409        return None;
410    }
411    let mut j = i + 1;
412    if chars.get(j).is_some_and(|c| c.is_alphabetic() || *c == '_') {
413        while chars
414            .get(j)
415            .is_some_and(|c| c.is_alphanumeric() || *c == '_')
416        {
417            j += 1;
418        }
419    }
420    (chars.get(j) == Some(&'$')).then_some(j + 1 - i)
421}
422
423/// Check the whole text before reading any statement: every string, quoted
424/// name and comment closes (the first that does not is the error), then
425/// every parenthesis is matched, nested at most [`MAX_NESTING`] deep.
426fn check(text: &str) -> Result<(), SqlError> {
427    let mut lexer = Lexer::new(text);
428    let mut open: Vec<usize> = Vec::new();
429    let mut unmatched: Option<SqlError> = None;
430    while let Some(token) = lexer.next()? {
431        if unmatched.is_some() {
432            continue;
433        }
434        if token.is_symbol("(") {
435            open.push(token.line);
436            if open.len() > MAX_NESTING {
437                unmatched = Some(SqlError::new(
438                    token.line,
439                    format!("parentheses nest more than {MAX_NESTING} deep"),
440                ));
441            }
442        } else if token.is_symbol(")") && open.pop().is_none() {
443            unmatched = Some(SqlError::new(token.line, "a `)` closes nothing"));
444        }
445    }
446    if let Some(error) = unmatched {
447        return Err(error);
448    }
449    match open.first() {
450        Some(line) => Err(SqlError::new(*line, "a `(` is not closed")),
451        None => Ok(()),
452    }
453}
454
455/// How far the start of a statement is from `CREATE [OR REPLACE]
456/// [TEMPORARY …] TABLE`, as [`Reader::statement`] reads it.
457#[derive(Clone, Copy, PartialEq, Eq)]
458enum Start {
459    Empty,
460    Create,
461    Or,
462    Modifiers,
463    Table,
464    Other,
465}
466
467/// Words between `CREATE` and `TABLE`.
468const TABLE_MODIFIERS: &[&str] = &[
469    "GLOBAL",
470    "LOCAL",
471    "TEMP",
472    "TEMPORARY",
473    "UNLOGGED",
474    "VIRTUAL",
475];
476
477impl Start {
478    fn then(self, token: &Token) -> Start {
479        let modifier = TABLE_MODIFIERS.iter().any(|w| token.is(w));
480        match self {
481            Start::Empty if token.is("CREATE") => Start::Create,
482            Start::Create if token.is("OR") => Start::Or,
483            Start::Or if token.is("REPLACE") => Start::Modifiers,
484            Start::Create | Start::Modifiers if modifier => Start::Modifiers,
485            Start::Create | Start::Modifiers if token.is("TABLE") => Start::Table,
486            Start::Table => Start::Table,
487            _ => Start::Other,
488        }
489    }
490}
491
492/// The tokens a statement that is not a `CREATE TABLE` keeps: enough for
493/// its note.
494const OTHER_TOKENS: usize = 8;
495
496/// The statements of a [`check`]ed text, one at a time: the tokens up to
497/// each `;` outside parentheses. A statement also ends, for scripts that
498/// leave the `;` out, at a `GO` line (SQL Server's batch separator) and
499/// before a `CREATE` that starts a line or follows a `)`. A `CREATE TABLE`
500/// statement keeps every token, up to [`MAX_TOKENS`]; any other keeps its
501/// first few.
502struct Statements {
503    lexer: Lexer,
504    /// The line of the last token read.
505    last_line: usize,
506    /// Whether the last token read was a `)`.
507    after_close: bool,
508    /// A `CREATE` read past the end of the statement before it.
509    pending: Option<Token>,
510}
511
512impl Statements {
513    fn new(text: &str) -> Self {
514        Statements {
515            lexer: Lexer::new(text),
516            last_line: 0,
517            after_close: false,
518            pending: None,
519        }
520    }
521
522    fn next(&mut self) -> Result<Option<Vec<Token>>, SqlError> {
523        let mut tokens: Vec<Token> = Vec::new();
524        let mut depth = 0usize;
525        let mut start = Start::Empty;
526        loop {
527            let token = match self.pending.take() {
528                Some(token) => token,
529                None => match self.lexer.next()? {
530                    Some(token) => token,
531                    None => break,
532                },
533            };
534            let starts_line = token.line > self.last_line;
535            let after_close = std::mem::replace(&mut self.after_close, token.is_symbol(")"));
536            self.last_line = token.line;
537            if token.is_symbol("(") {
538                depth += 1;
539            } else if token.is_symbol(")") {
540                depth = depth.saturating_sub(1);
541            } else if depth == 0 && (token.is_symbol(";") || (token.is("GO") && starts_line)) {
542                if tokens.is_empty() {
543                    continue;
544                }
545                return Ok(Some(tokens));
546            } else if depth == 0
547                && token.is("CREATE")
548                && (starts_line || after_close)
549                && !tokens.is_empty()
550            {
551                self.pending = Some(token);
552                return Ok(Some(tokens));
553            }
554            if start != Start::Other {
555                start = start.then(&token);
556            }
557            if start == Start::Other && tokens.len() >= OTHER_TOKENS {
558                continue;
559            }
560            if tokens.len() == MAX_TOKENS {
561                return Err(SqlError::new(
562                    tokens[0].line,
563                    format!("a statement of more than {MAX_TOKENS} tokens"),
564                ));
565            }
566            tokens.push(token);
567        }
568        Ok((!tokens.is_empty()).then_some(tokens))
569    }
570}
571
572/// `tokens` split at commas outside parentheses.
573fn split_commas(tokens: &[Token]) -> Vec<&[Token]> {
574    let mut out = Vec::new();
575    let mut depth = 0usize;
576    let mut start = 0;
577    for (index, token) in tokens.iter().enumerate() {
578        if token.is_symbol("(") {
579            depth += 1;
580        } else if token.is_symbol(")") {
581            depth = depth.saturating_sub(1);
582        } else if token.is_symbol(",") && depth == 0 {
583            out.push(&tokens[start..index]);
584            start = index + 1;
585        }
586    }
587    out.push(&tokens[start..]);
588    out
589}
590
591/// The index just past the parenthesis that closes the one at `open`.
592fn closing(tokens: &[Token], open: usize) -> usize {
593    let mut depth = 0usize;
594    for (index, token) in tokens.iter().enumerate().skip(open) {
595        if token.is_symbol("(") {
596            depth += 1;
597        } else if token.is_symbol(")") {
598            depth = depth.saturating_sub(1);
599            if depth == 0 {
600                return index + 1;
601            }
602        }
603    }
604    tokens.len()
605}
606
607/// Tokens written back as SQL: words spaced, punctuation close.
608fn write(tokens: &[Token]) -> String {
609    let mut out = String::new();
610    for (index, token) in tokens.iter().enumerate() {
611        if index > 0 && space_before(tokens, index) {
612            out.push(' ');
613        }
614        out.push_str(&token.text);
615    }
616    out
617}
618
619/// Whether [`write`] puts a space before token `index`.
620fn space_before(tokens: &[Token], index: usize) -> bool {
621    let (prev, token) = (&tokens[index - 1], &tokens[index]);
622    let symbol = |t: &Token, any: &[&str]| t.kind == Kind::Symbol && any.contains(&t.text.as_str());
623    if symbol(token, &[")", ",", "[", "]", "[]", "::", "."]) || symbol(prev, &["(", "[", "::", "."])
624    {
625        return false;
626    }
627    // A call or a type's parameters: `now()`, `NUMERIC(10, 2)`.
628    if token.is_symbol("(") && prev.is_name() {
629        return false;
630    }
631    // A sign that starts a value stays with its number: `-1`, `(-1`.
632    let starts = index == 1 || symbol(&tokens[index - 2], &["(", ","]);
633    !(symbol(prev, &["-", "+"]) && token.kind == Kind::Number && starts)
634}
635
636/// The names in a parenthesised list starting at `open`, and the index past
637/// it.
638fn name_list(tokens: &[Token], open: usize) -> (Vec<String>, usize) {
639    let end = closing(tokens, open);
640    let inner = &tokens[(open + 1).min(end)..end.saturating_sub(1).max(open + 1)];
641    // One past the most a key may have, for the caller to refuse.
642    let names = split_commas(inner)
643        .into_iter()
644        .filter_map(|part| part.iter().find(|t| t.is_name()).map(|t| t.value.clone()))
645        .take(MAX_COLUMNS + 1)
646        .collect();
647    (names, end)
648}
649
650/// `columns`, unless there are more than a table may have.
651fn bounded(table: &str, line: usize, columns: Vec<String>) -> Result<Vec<String>, SqlError> {
652    if columns.len() > MAX_COLUMNS {
653        return Err(SqlError::new(
654            line,
655            format!("{table}: a key of more than {MAX_COLUMNS} columns"),
656        ));
657    }
658    Ok(columns)
659}
660
661/// A possibly qualified name (`a.b.c`) at `at`, and the index past it.
662fn qualified(tokens: &[Token], mut at: usize) -> Option<(String, usize)> {
663    let mut parts = Vec::new();
664    loop {
665        let token = tokens.get(at).filter(|t| t.is_name())?;
666        parts.push(token.value.clone());
667        at += 1;
668        if tokens.get(at).is_some_and(|t| t.is_symbol(".")) {
669            at += 1;
670        } else {
671            return Some((parts.join("."), at));
672        }
673    }
674}
675
676// --------------------------------------------------------------- statements
677
678struct Reader {
679    schema: Schema,
680    /// Each table's index in `schema`, by name.
681    index: HashMap<String, usize>,
682    notes: Vec<Note>,
683    dropped: usize,
684}
685
686/// Words that end a column's type and start its constraints.
687const COLUMN_KEYWORDS: &[&str] = &[
688    "CONSTRAINT",
689    "NOT",
690    "NULL",
691    "DEFAULT",
692    "PRIMARY",
693    "UNIQUE",
694    "REFERENCES",
695    "CHECK",
696    "COLLATE",
697    "GENERATED",
698    "AS",
699    "AUTO_INCREMENT",
700    "AUTOINCREMENT",
701    "IDENTITY",
702    "COMMENT",
703    "ON",
704    "CHARACTER",
705    "CHARSET",
706];
707
708impl Reader {
709    fn note(&mut self, line: usize, message: impl Into<String>) {
710        if self.notes.len() < MAX_NOTES {
711            self.notes.push(Note {
712                line,
713                message: message.into(),
714            });
715        } else {
716            self.dropped += 1;
717        }
718    }
719
720    fn statement(&mut self, tokens: &[Token]) -> Result<(), SqlError> {
721        let line = tokens[0].line;
722        let mut at = 0;
723        let word = |at: usize, w: &str| tokens.get(at).is_some_and(|t| t.is(w));
724        if !word(0, "CREATE") {
725            let what = tokens[0].text.to_uppercase();
726            self.note(line, format!("{what} statement skipped"));
727            return Ok(());
728        }
729        at += 1;
730        if word(at, "OR") && word(at + 1, "REPLACE") {
731            at += 2;
732        }
733        while TABLE_MODIFIERS.iter().any(|w| word(at, w)) {
734            at += 1;
735        }
736        if !word(at, "TABLE") {
737            // `CREATE UNIQUE INDEX`: the words up to the kind of object.
738            let objects = [
739                "INDEX",
740                "VIEW",
741                "SEQUENCE",
742                "TYPE",
743                "FUNCTION",
744                "TRIGGER",
745                "SCHEMA",
746                "EXTENSION",
747                "DATABASE",
748                "PROCEDURE",
749                "DOMAIN",
750            ];
751            let mut what: Vec<String> = Vec::new();
752            for token in tokens.iter().take_while(|t| t.kind == Kind::Word).take(5) {
753                what.push(token.text.to_uppercase());
754                if objects.iter().any(|w| token.is(w)) {
755                    break;
756                }
757            }
758            self.note(line, format!("{} statement skipped", what.join(" ")));
759            return Ok(());
760        }
761        at += 1;
762        if word(at, "IF") && word(at + 1, "NOT") && word(at + 2, "EXISTS") {
763            at += 3;
764        }
765        let Some((name, after)) = qualified(tokens, at) else {
766            let line = tokens.get(at).map_or(line, |t| t.line);
767            return Err(SqlError::new(line, "CREATE TABLE needs a table name"));
768        };
769        at = after;
770        if word(at, "AS") || word(at, "LIKE") {
771            let what = tokens[at].text.to_uppercase();
772            self.note(line, format!("{name}: CREATE TABLE … {what} skipped"));
773            return Ok(());
774        }
775        if !tokens.get(at).is_some_and(|t| t.is_symbol("(")) {
776            let line = tokens.get(at).map_or(line, |t| t.line);
777            return Err(SqlError::new(
778                line,
779                format!("CREATE TABLE {name} needs a column list in parentheses"),
780            ));
781        }
782        let end = closing(tokens, at);
783        let body = &tokens[at + 1..end - 1];
784        let table = self.table(&name, body, line)?;
785        if end < tokens.len() {
786            // Table options (`ENGINE=InnoDB`, `WITHOUT ROWID`, `STRICT`):
787            // nothing the model holds.
788            let options = &tokens[end..];
789            let comment = options
790                .iter()
791                .position(|t| t.is("COMMENT"))
792                .and_then(|at| options[at + 1..].iter().find(|t| !t.is_symbol("=")))
793                .filter(|t| t.kind == Kind::Str)
794                .map(|t| t.value.clone());
795            let table = match comment {
796                Some(comment) => table.with_description(comment),
797                None => table,
798            };
799            self.add(table, line);
800        } else {
801            self.add(table, line);
802        }
803        Ok(())
804    }
805
806    fn add(&mut self, table: Schema, line: usize) {
807        let name = table.name().unwrap_or_default().to_string();
808        if let Some(&index) = self.index.get(&name) {
809            self.note(
810                line,
811                format!("{name}: defined again; the later definition is kept"),
812            );
813            self.schema.tables_mut()[index] = table;
814        } else {
815            self.index.insert(name, self.schema.tables().len());
816            self.schema.push_table(table);
817        }
818    }
819
820    fn table(&mut self, name: &str, body: &[Token], line: usize) -> Result<Schema, SqlError> {
821        let parts = split_commas(body);
822        if parts.len() > MAX_COLUMNS {
823            return Err(SqlError::new(
824                line,
825                format!("{name}: more than {MAX_COLUMNS} columns"),
826            ));
827        }
828        let mut table = Schema::default().named(name);
829        let mut keys: Vec<(usize, TableKey)> = Vec::new();
830        for part in parts {
831            let Some(first) = part.first() else {
832                continue;
833            };
834            let mut at = 0;
835            if first.is("CONSTRAINT") {
836                at = 2;
837            }
838            let Some(head) = part.get(at) else {
839                return Err(SqlError::new(
840                    first.line,
841                    format!("{name}: CONSTRAINT needs a rule"),
842                ));
843            };
844            let word = |at: usize, w: &str| part.get(at).is_some_and(|t| t.is(w));
845            let open = |at: usize| part.get(at).is_some_and(|t| t.is_symbol("("));
846            if head.is("PRIMARY") && word(at + 1, "KEY") {
847                // `PRIMARY KEY [CLUSTERED] (columns)`.
848                let mut open_at = at + 2;
849                while part.get(open_at).is_some_and(|t| t.kind == Kind::Word) {
850                    open_at += 1;
851                }
852                if !open(open_at) {
853                    return Err(SqlError::new(
854                        head.line,
855                        format!("{name}: PRIMARY KEY needs its columns"),
856                    ));
857                }
858                let columns = bounded(name, head.line, name_list(part, open_at).0)?;
859                keys.push((head.line, TableKey::Primary(columns)));
860            } else if head.is("UNIQUE") {
861                let mut open_at = at + 1;
862                while part.get(open_at).is_some_and(|t| !t.is_symbol("(")) {
863                    open_at += 1;
864                }
865                if !open(open_at) {
866                    return Err(SqlError::new(
867                        head.line,
868                        format!("{name}: UNIQUE needs its columns"),
869                    ));
870                }
871                let columns = bounded(name, head.line, name_list(part, open_at).0)?;
872                keys.push((head.line, TableKey::Unique(columns)));
873            } else if head.is("FOREIGN") && word(at + 1, "KEY") {
874                // `FOREIGN KEY [name] (columns) REFERENCES …`.
875                let mut open_at = at + 2;
876                while part
877                    .get(open_at)
878                    .is_some_and(|t| !t.is_symbol("(") && !t.is("REFERENCES"))
879                {
880                    open_at += 1;
881                }
882                if !open(open_at) {
883                    return Err(SqlError::new(
884                        head.line,
885                        format!("{name}: FOREIGN KEY needs its columns"),
886                    ));
887                }
888                let (columns, after) = name_list(part, open_at);
889                let columns = bounded(name, head.line, columns)?;
890                let Some(key) = references(part, after) else {
891                    return Err(SqlError::new(
892                        head.line,
893                        format!("{name}: FOREIGN KEY needs REFERENCES and a table"),
894                    ));
895                };
896                let references = bounded(name, head.line, key.references)?;
897                let key = ForeignKey {
898                    columns,
899                    references,
900                    ..key
901                };
902                keys.push((head.line, TableKey::Foreign(key)));
903            } else if head.is("CHECK") {
904                self.note(head.line, format!("{name}: CHECK constraint skipped"));
905            } else if [
906                "KEY", "INDEX", "FULLTEXT", "SPATIAL", "EXCLUDE", "LIKE", "PERIOD",
907            ]
908            .iter()
909            .any(|w| head.is(w))
910                && !(at == 0 && named_like_a_clause(part))
911            {
912                let what = head.text.to_uppercase();
913                self.note(head.line, format!("{name}: {what} clause skipped"));
914            } else if at == 0 && head.is_name() {
915                let field = self.column(name, part)?;
916                table.push(field);
917            } else {
918                return Err(SqlError::new(
919                    head.line,
920                    format!(
921                        "{name}: expected a column or a constraint, found `{}`",
922                        head.text
923                    ),
924                ));
925            }
926        }
927        for (line, key) in keys {
928            self.apply(&mut table, key, line);
929        }
930        Ok(table)
931    }
932
933    /// Put a table-level key on its column, or on the table when it spans
934    /// several.
935    fn apply(&mut self, table: &mut Schema, key: TableKey, line: usize) {
936        let columns = key.columns().to_vec();
937        let name = table.name().unwrap_or_default().to_string();
938        for column in &columns {
939            if table.field(column).is_none() {
940                self.note(line, format!("{name}: a key names no column {column:?}"));
941                return;
942            }
943        }
944        let single = columns.len() == 1;
945        let index = |table: &Schema, column: &str| table.index_of(column).unwrap_or(0);
946        match key {
947            TableKey::Primary(columns) => {
948                for column in &columns {
949                    let i = index(table, column);
950                    let field = &mut table.fields_mut()[i];
951                    *field = field.clone().required(true).nullable(false);
952                    if single && !field.is_primary_key() {
953                        field
954                            .constraints_mut()
955                            .push(Constraint::PrimaryKey(Vec::new()));
956                    }
957                }
958                if !single {
959                    table
960                        .constraints_mut()
961                        .push(Constraint::PrimaryKey(columns));
962                }
963            }
964            TableKey::Unique(columns) if single => {
965                let i = index(table, &columns[0]);
966                let field = &mut table.fields_mut()[i];
967                if !field.is_unique() {
968                    field.constraints_mut().push(Constraint::Unique(Vec::new()));
969                }
970            }
971            TableKey::Unique(columns) => table.constraints_mut().push(Constraint::Unique(columns)),
972            TableKey::Foreign(key) if single => {
973                let i = index(table, &key.columns[0]);
974                let field = &mut table.fields_mut()[i];
975                field
976                    .constraints_mut()
977                    .push(Constraint::References(ForeignKey {
978                        columns: Vec::new(),
979                        ..key
980                    }));
981            }
982            TableKey::Foreign(key) => table.constraints_mut().push(Constraint::References(key)),
983        }
984    }
985
986    fn column(&mut self, table: &str, part: &[Token]) -> Result<Field, SqlError> {
987        let name = &part[0];
988        // The type: everything up to the first constraint keyword.
989        let mut at = 1;
990        while at < part.len() {
991            let token = &part[at];
992            if token.is_symbol("(") {
993                at = closing(part, at);
994                continue;
995            }
996            if COLUMN_KEYWORDS.iter().any(|w| token.is(w)) {
997                // `CHARACTER VARYING` and `DOUBLE PRECISION` are types; a
998                // `CHARACTER SET` after a type is not.
999                let character_type = token.is("CHARACTER") && at == 1;
1000                if !character_type {
1001                    break;
1002                }
1003            }
1004            at += 1;
1005        }
1006        let type_tokens = &part[1..at];
1007        let native = write(type_tokens);
1008        let mut field = Field::new(name.value.clone(), sql_type(type_tokens));
1009        if !native.is_empty() {
1010            field = field.with_native_type(native.clone());
1011        }
1012        if let Some(values) = enum_values(type_tokens) {
1013            field = field.with_constraint(Constraint::Enum(values));
1014        }
1015        while at < part.len() {
1016            let token = &part[at];
1017            let word = |offset: usize, w: &str| part.get(at + offset).is_some_and(|t| t.is(w));
1018            if token.is("CONSTRAINT") {
1019                at += 2;
1020            } else if token.is("NOT") && word(1, "NULL") {
1021                field = field.nullable(false).required(true);
1022                at += 2;
1023            } else if token.is("NULL") {
1024                field = field.nullable(true);
1025                at += 1;
1026            } else if token.is("DEFAULT") {
1027                let end = expression_end(part, at + 1);
1028                let text = write(&part[at + 1..end]);
1029                field = field.with_constraint(Constraint::Default(Literal::new(text)));
1030                at = end;
1031            } else if token.is("PRIMARY") && word(1, "KEY") {
1032                field = field
1033                    .nullable(false)
1034                    .required(true)
1035                    .with_constraint(Constraint::PrimaryKey(Vec::new()));
1036                at += 2;
1037            } else if token.is("UNIQUE") {
1038                field = field.with_constraint(Constraint::Unique(Vec::new()));
1039                at += if word(1, "KEY") { 2 } else { 1 };
1040            } else if token.is("REFERENCES") {
1041                match references(part, at) {
1042                    Some(mut key) => {
1043                        key.references = bounded(table, token.line, key.references)?;
1044                        field = field.with_constraint(Constraint::References(key));
1045                    }
1046                    None => {
1047                        return Err(SqlError::new(
1048                            token.line,
1049                            format!("{table}.{}: REFERENCES needs a table", name.value),
1050                        ));
1051                    }
1052                }
1053                at = skip_reference(part, at);
1054            } else if token.is("CHECK") {
1055                self.note(
1056                    token.line,
1057                    format!("{table}.{}: CHECK constraint skipped", name.value),
1058                );
1059                at += 1;
1060                if part.get(at).is_some_and(|t| t.is_symbol("(")) {
1061                    at = closing(part, at);
1062                }
1063            } else if token.is("COMMENT") && part.get(at + 1).is_some_and(|t| t.kind == Kind::Str) {
1064                field = field.with_description(part[at + 1].value.clone());
1065                at += 2;
1066            } else if token.is_symbol("(") {
1067                at = closing(part, at);
1068            } else {
1069                // `COLLATE x`, `GENERATED … AS (…)`, `AUTO_INCREMENT`,
1070                // `ON UPDATE …`: nothing the model holds.
1071                at += 1;
1072            }
1073        }
1074        Ok(field)
1075    }
1076}
1077
1078/// Whether a table element that starts with a clause's word (`KEY`,
1079/// `INDEX`, `PERIOD`, …) is a column of that name: alone, or followed by a
1080/// type (`key TEXT`, `index INT NOT NULL`), where a clause is followed by
1081/// a name or a parenthesis (`KEY ix (a)`, `INDEX (a)`).
1082fn named_like_a_clause(part: &[Token]) -> bool {
1083    match part.get(1) {
1084        None => true,
1085        Some(next) => next.kind == Kind::Word && sql_type(&part[1..2]) != DataType::Unknown,
1086    }
1087}
1088
1089/// A table-level key, before it is placed.
1090enum TableKey {
1091    Primary(Vec<String>),
1092    Unique(Vec<String>),
1093    Foreign(ForeignKey),
1094}
1095
1096impl TableKey {
1097    fn columns(&self) -> &[String] {
1098        match self {
1099            TableKey::Primary(columns) | TableKey::Unique(columns) => columns,
1100            TableKey::Foreign(key) => &key.columns,
1101        }
1102    }
1103}
1104
1105/// `REFERENCES table [(columns)]` at `at` (the `REFERENCES` keyword, or
1106/// the token before it).
1107fn references(tokens: &[Token], mut at: usize) -> Option<ForeignKey> {
1108    while tokens.get(at).is_some_and(|t| !t.is("REFERENCES")) {
1109        at += 1;
1110    }
1111    let (table, after) = qualified(tokens, at + 1)?;
1112    let references = if tokens.get(after).is_some_and(|t| t.is_symbol("(")) {
1113        name_list(tokens, after).0
1114    } else {
1115        Vec::new()
1116    };
1117    Some(ForeignKey {
1118        columns: Vec::new(),
1119        table,
1120        references,
1121    })
1122}
1123
1124/// The index past a `REFERENCES` clause and its `ON DELETE …`, `MATCH …`
1125/// and `DEFERRABLE …` options.
1126fn skip_reference(tokens: &[Token], at: usize) -> usize {
1127    let Some((_, mut at)) = qualified(tokens, at + 1) else {
1128        return tokens.len();
1129    };
1130    if tokens.get(at).is_some_and(|t| t.is_symbol("(")) {
1131        at = closing(tokens, at);
1132    }
1133    let options = [
1134        "ON",
1135        "DELETE",
1136        "UPDATE",
1137        "CASCADE",
1138        "RESTRICT",
1139        "SET",
1140        "NULL",
1141        "DEFAULT",
1142        "NO",
1143        "ACTION",
1144        "MATCH",
1145        "FULL",
1146        "PARTIAL",
1147        "SIMPLE",
1148        "DEFERRABLE",
1149        "INITIALLY",
1150        "DEFERRED",
1151        "IMMEDIATE",
1152        "NOT",
1153    ];
1154    while let Some(token) = tokens.get(at) {
1155        // `NOT NULL` after a reference is the column's, not the reference's.
1156        if token.is("NOT") && tokens.get(at + 1).is_some_and(|t| t.is("NULL")) {
1157            break;
1158        }
1159        // So are a `NULL` and a `DEFAULT` that do not follow `SET`.
1160        let after_set = tokens.get(at.wrapping_sub(1)).is_some_and(|t| t.is("SET"));
1161        if (token.is("NULL") || token.is("DEFAULT")) && !after_set {
1162            break;
1163        }
1164        if options.iter().any(|w| token.is(w)) {
1165            at += 1;
1166        } else {
1167            break;
1168        }
1169    }
1170    at
1171}
1172
1173/// The index past a `DEFAULT` expression starting at `at`: up to the next
1174/// constraint keyword outside parentheses.
1175fn expression_end(tokens: &[Token], mut at: usize) -> usize {
1176    let start = at;
1177    while let Some(token) = tokens.get(at) {
1178        if token.is_symbol("(") {
1179            at = closing(tokens, at);
1180            continue;
1181        }
1182        let keyword = [
1183            "NOT",
1184            "NULL",
1185            "PRIMARY",
1186            "UNIQUE",
1187            "REFERENCES",
1188            "CHECK",
1189            "CONSTRAINT",
1190            "COLLATE",
1191            "GENERATED",
1192            "COMMENT",
1193            "AUTO_INCREMENT",
1194            "AUTOINCREMENT",
1195            "ON",
1196        ]
1197        .iter()
1198        .any(|w| token.is(w));
1199        // `DEFAULT NULL` is the value NULL.
1200        if keyword && at > start {
1201            break;
1202        }
1203        at += 1;
1204    }
1205    at
1206}
1207
1208/// MySQL's `ENUM('a', 'b')` values.
1209fn enum_values(type_tokens: &[Token]) -> Option<Vec<Literal>> {
1210    let first = type_tokens.first()?;
1211    if !(first.is("ENUM") || first.is("SET")) {
1212        return None;
1213    }
1214    let values: Vec<Literal> = type_tokens
1215        .iter()
1216        .filter(|t| t.kind == Kind::Str)
1217        .map(|t| Literal::new(t.text.clone()))
1218        .collect();
1219    (!values.is_empty()).then_some(values)
1220}
1221
1222/// The model's type for a SQL type.
1223fn sql_type(type_tokens: &[Token]) -> DataType {
1224    let words: Vec<String> = type_tokens
1225        .iter()
1226        .filter(|t| t.kind == Kind::Word)
1227        .map(|t| t.text.to_uppercase())
1228        .collect();
1229    let Some(base) = words.first() else {
1230        return DataType::Unknown;
1231    };
1232    let array = words.iter().any(|w| w == "ARRAY")
1233        || type_tokens
1234            .iter()
1235            .any(|t| t.is_symbol("[]") || t.is_symbol("["));
1236    let has = |word: &str| words.iter().any(|w| w == word);
1237    let numbers: Vec<i64> = type_tokens
1238        .iter()
1239        .filter(|t| t.kind == Kind::Number)
1240        .filter_map(|t| t.text.parse().ok())
1241        .collect();
1242    let scalar = match base.as_str() {
1243        "INT" | "INTEGER" | "SMALLINT" | "BIGINT" | "TINYINT" | "MEDIUMINT" | "SERIAL"
1244        | "BIGSERIAL" | "SMALLSERIAL" | "INT2" | "INT4" | "INT8" | "SERIAL4" | "SERIAL8"
1245        | "UNSIGNED" => DataType::Integer,
1246        "REAL" | "FLOAT" | "FLOAT4" | "FLOAT8" | "DOUBLE" => DataType::Float,
1247        "DECIMAL" | "NUMERIC" | "DEC" | "NUMBER" | "MONEY" | "SMALLMONEY" => DataType::Decimal {
1248            precision: numbers.first().and_then(|&p| u32::try_from(p).ok()),
1249            scale: numbers.get(1).and_then(|&s| i32::try_from(s).ok()),
1250        },
1251        "CHAR" | "VARCHAR" | "CHARACTER" | "NCHAR" | "NVARCHAR" | "VARCHAR2" | "NVARCHAR2"
1252        | "TEXT" | "TINYTEXT" | "MEDIUMTEXT" | "LONGTEXT" | "NTEXT" | "CLOB" | "NCLOB"
1253        | "STRING" | "CITEXT" | "UUID" | "UNIQUEIDENTIFIER" | "ENUM" | "SET" | "INET" | "CIDR"
1254        | "MACADDR" | "XML" => DataType::String,
1255        "BOOLEAN" | "BOOL" => DataType::Boolean,
1256        "BLOB" | "TINYBLOB" | "MEDIUMBLOB" | "LONGBLOB" | "BYTEA" | "BINARY" | "VARBINARY"
1257        | "IMAGE" | "RAW" => DataType::Binary,
1258        "DATE" => DataType::Date,
1259        "TIMESTAMPTZ" => DataType::Timestamp {
1260            timezone: Some("UTC".into()),
1261        },
1262        "TIMESTAMP" | "DATETIME" | "DATETIME2" | "SMALLDATETIME" | "DATETIMEOFFSET" => {
1263            let zoned = (has("WITH") && !has("WITHOUT")) || base == "DATETIMEOFFSET";
1264            DataType::Timestamp {
1265                timezone: zoned.then(|| "UTC".to_string()),
1266            }
1267        }
1268        "JSON" | "JSONB" | "VARIANT" => DataType::Any,
1269        _ => DataType::Unknown,
1270    };
1271    if array {
1272        DataType::list(scalar)
1273    } else {
1274        scalar
1275    }
1276}
1277
1278#[cfg(test)]
1279mod tests {
1280    use super::*;
1281
1282    fn notes(parsed: &Parsed) -> Vec<String> {
1283        parsed.notes.iter().map(ToString::to_string).collect()
1284    }
1285
1286    #[test]
1287    fn columns_types_and_keys() {
1288        let parsed = parse(
1289            r#"
1290            CREATE TABLE IF NOT EXISTS "public"."order items" (
1291                "order" BIGINT NOT NULL REFERENCES orders (id) ON DELETE CASCADE,
1292                line INT NOT NULL,
1293                price NUMERIC(10, 2) DEFAULT 0.00 NOT NULL,
1294                status ENUM('new', 'paid') DEFAULT 'new' COMMENT 'Where it is',
1295                tags TEXT[],
1296                at TIMESTAMP WITH TIME ZONE DEFAULT now(),
1297                weight DOUBLE PRECISION NULL,
1298                code CHARACTER VARYING(8) UNIQUE,
1299                PRIMARY KEY ("order", line),
1300                UNIQUE (line, code),
1301                CONSTRAINT fk FOREIGN KEY (line, code) REFERENCES lines (n, c)
1302            ) ENGINE=InnoDB COMMENT 'Lines';
1303            "#,
1304        )
1305        .unwrap();
1306        assert!(parsed.notes.is_empty(), "{:?}", parsed.notes);
1307        let table = &parsed.schema.tables()[0];
1308        assert_eq!(table.name(), Some("public.order items"));
1309        assert_eq!(table.description(), Some("Lines"));
1310        let types: Vec<(String, String)> = table
1311            .fields()
1312            .iter()
1313            .map(|f| {
1314                (
1315                    f.native_type().unwrap().to_string(),
1316                    f.data_type().to_string(),
1317                )
1318            })
1319            .collect();
1320        let types: Vec<(&str, &str)> = types
1321            .iter()
1322            .map(|(a, b)| (a.as_str(), b.as_str()))
1323            .collect();
1324        assert_eq!(
1325            types,
1326            [
1327                ("BIGINT", "integer"),
1328                ("INT", "integer"),
1329                ("NUMERIC(10, 2)", "decimal(10,2)"),
1330                ("ENUM('new', 'paid')", "string"),
1331                ("TEXT[]", "list<string>"),
1332                ("TIMESTAMP WITH TIME ZONE", "timestamp[UTC]"),
1333                ("DOUBLE PRECISION", "float"),
1334                ("CHARACTER VARYING(8)", "string"),
1335            ]
1336        );
1337        let order = table.field("order").unwrap();
1338        assert!(order.is_required() && !order.is_nullable());
1339        assert_eq!(order.references().unwrap().to_string(), "orders.id");
1340        assert_eq!(table.primary_key(), ["order", "line"]);
1341        let price = table.field("price").unwrap();
1342        assert_eq!(price.default_value().unwrap().as_str(), "0.00");
1343        assert!(price.is_required());
1344        let status = table.field("status").unwrap();
1345        assert_eq!(status.default_value().unwrap().as_str(), "'new'");
1346        assert_eq!(status.description(), Some("Where it is"));
1347        assert_eq!(status.enum_values().unwrap().len(), 2);
1348        assert_eq!(
1349            table.field("at").unwrap().default_value().unwrap().as_str(),
1350            "now()"
1351        );
1352        assert!(table.field("code").unwrap().is_unique());
1353        let shown: Vec<String> = table
1354            .constraints()
1355            .iter()
1356            .map(ToString::to_string)
1357            .collect();
1358        assert_eq!(
1359            shown,
1360            [
1361                "primary key (order, line)",
1362                "unique (line, code)",
1363                "→ (line, code) → lines.(n, c)",
1364            ]
1365        );
1366    }
1367
1368    #[test]
1369    fn a_default_after_a_reference_is_the_columns() {
1370        let parsed = parse(
1371            "CREATE TABLE t (\
1372             a INT REFERENCES p (id) DEFAULT 0, \
1373             b INT REFERENCES p (id) ON DELETE SET DEFAULT DEFAULT 1 NOT NULL, \
1374             c INT REFERENCES p ON UPDATE SET NULL NULL);",
1375        )
1376        .unwrap();
1377        let t = &parsed.schema.tables()[0];
1378        let a = t.field("a").unwrap();
1379        assert_eq!(a.default_value().unwrap().as_str(), "0");
1380        assert_eq!(a.references().unwrap().to_string(), "p.id");
1381        let b = t.field("b").unwrap();
1382        assert_eq!(b.default_value().unwrap().as_str(), "1");
1383        assert!(b.is_required());
1384        let c = t.field("c").unwrap();
1385        assert!(c.default_value().is_none() && c.is_nullable());
1386    }
1387
1388    #[test]
1389    fn single_column_table_keys_go_on_the_column() {
1390        let parsed = parse(
1391            "CREATE TABLE t (a INT, b INT, PRIMARY KEY (a), UNIQUE (b), \
1392             FOREIGN KEY (b) REFERENCES u);",
1393        )
1394        .unwrap();
1395        let t = &parsed.schema.tables()[0];
1396        assert!(t.constraints().is_empty());
1397        assert!(t.field("a").unwrap().is_primary_key());
1398        assert!(t.field("a").unwrap().is_required());
1399        assert!(t.field("b").unwrap().is_unique());
1400        assert_eq!(t.foreign_keys()[0].to_string(), "b → u");
1401    }
1402
1403    #[test]
1404    fn other_statements_and_clauses_are_noted() {
1405        let parsed = parse(
1406            "/* a dump */\n\
1407             SET NAMES utf8;\n\
1408             CREATE TABLE a (x INT CHECK (x > 0), KEY ix (x));\n\
1409             CREATE UNIQUE INDEX i ON a (x);\n\
1410             CREATE TABLE b AS SELECT * FROM a;\n\
1411             CREATE TABLE a (y TEXT);\n\
1412             INSERT INTO a VALUES (1);",
1413        )
1414        .unwrap();
1415        assert_eq!(
1416            notes(&parsed),
1417            [
1418                "line 2: SET statement skipped",
1419                "line 3: a.x: CHECK constraint skipped",
1420                "line 3: a: KEY clause skipped",
1421                "line 4: CREATE UNIQUE INDEX statement skipped",
1422                "line 5: b: CREATE TABLE … AS skipped",
1423                "line 6: a: defined again; the later definition is kept",
1424                "line 7: INSERT statement skipped",
1425            ]
1426        );
1427        assert_eq!(parsed.schema.tables().len(), 1);
1428        assert_eq!(parsed.schema.tables()[0].fields()[0].name(), "y");
1429    }
1430
1431    #[test]
1432    fn errors_name_their_line() {
1433        let cases = [
1434            ("CREATE TABLE t (\n a INT", "line 1: a `(` is not closed"),
1435            ("\n\nSELECT 'oops", "line 3: a string is not closed"),
1436            ("/* never\nclosed", "line 1: a /* comment is not closed"),
1437            (
1438                "CREATE TABLE t\n;",
1439                "line 1: CREATE TABLE t needs a column list in parentheses",
1440            ),
1441            (
1442                "CREATE TABLE (a INT);",
1443                "line 1: CREATE TABLE needs a table name",
1444            ),
1445            ("x)", "line 1: a `)` closes nothing"),
1446            (
1447                "CREATE TABLE t (a INT REFERENCES);",
1448                "line 1: t.a: REFERENCES needs a table",
1449            ),
1450            (
1451                "CREATE TABLE t (\n, PRIMARY KEY);",
1452                "line 2: t: PRIMARY KEY needs its columns",
1453            ),
1454        ];
1455        for (ddl, expected) in cases {
1456            assert_eq!(parse(ddl).unwrap_err().to_string(), expected, "{ddl}");
1457        }
1458    }
1459
1460    #[test]
1461    fn limits_hold() {
1462        let deep = format!(
1463            "CREATE TABLE t (a INT DEFAULT {}1{});",
1464            "(".repeat(100),
1465            ")".repeat(100)
1466        );
1467        assert!(parse(&deep).unwrap_err().message().contains("nest"));
1468        let wide: Vec<String> = (0..=MAX_COLUMNS).map(|i| format!("c{i} INT")).collect();
1469        let wide = format!("CREATE TABLE t ({});", wide.join(", "));
1470        assert!(parse(&wide).unwrap_err().message().contains("columns"));
1471        let mut many = String::new();
1472        for i in 0..1_200 {
1473            many.push_str(&format!("INSERT INTO t VALUES ({i});\n"));
1474        }
1475        let parsed = parse(&many).unwrap();
1476        assert_eq!(parsed.notes.len(), MAX_NOTES + 1);
1477        assert_eq!(parsed.notes.last().unwrap().message, "… and 200 more notes");
1478    }
1479
1480    #[test]
1481    fn a_foreign_key_without_columns_is_an_error() {
1482        for ddl in [
1483            "CREATE TABLE t (a INT, FOREIGN KEY REFERENCES u)",
1484            "CREATE TABLE t (a INT, FOREIGN KEY)",
1485            "CREATE TABLE t (a INT, CONSTRAINT f FOREIGN KEY REFERENCES u (id))",
1486        ] {
1487            let error = parse(ddl).unwrap_err();
1488            assert_eq!(
1489                error.to_string(),
1490                "line 1: t: FOREIGN KEY needs its columns",
1491                "{ddl}"
1492            );
1493        }
1494    }
1495
1496    #[test]
1497    fn columns_named_like_index_clauses_are_columns() {
1498        let parsed = parse(
1499            "CREATE TABLE settings (key TEXT PRIMARY KEY, index INT NOT NULL, \
1500             period VARCHAR(10), KEY ix (index), INDEX (period), KEY (key));",
1501        )
1502        .unwrap();
1503        let t = &parsed.schema.tables()[0];
1504        let names: Vec<&str> = t.fields().iter().map(Field::name).collect();
1505        assert_eq!(names, ["key", "index", "period"]);
1506        assert_eq!(t.primary_key(), ["key"]);
1507        assert!(t.field("index").unwrap().is_required());
1508        assert_eq!(
1509            notes(&parsed),
1510            [
1511                "line 1: settings: KEY clause skipped",
1512                "line 1: settings: INDEX clause skipped",
1513                "line 1: settings: KEY clause skipped",
1514            ]
1515        );
1516    }
1517
1518    #[test]
1519    fn many_tables_parse_in_linear_time() {
1520        // Each table was looked up among all those before it: 100,000
1521        // tables took minutes.
1522        let mut ddl = String::new();
1523        for i in 0..MAX_STATEMENTS - 1 {
1524            ddl.push_str(&format!("CREATE TABLE t{i} (a INT);\n"));
1525        }
1526        ddl.push_str("CREATE TABLE t7 (b INT);");
1527        let started = std::time::Instant::now();
1528        let parsed = parse(&ddl).unwrap();
1529        assert!(started.elapsed() < std::time::Duration::from_secs(30));
1530        assert_eq!(parsed.schema.tables().len(), MAX_STATEMENTS - 1);
1531        assert_eq!(parsed.schema.tables()[7].fields()[0].name(), "b");
1532        assert_eq!(
1533            notes(&parsed),
1534            [format!(
1535                "line {MAX_STATEMENTS}: t7: defined again; the later definition is kept"
1536            )]
1537        );
1538        ddl.push_str("\nCREATE TABLE u (a INT);");
1539        let error = parse(&ddl).unwrap_err();
1540        assert_eq!(
1541            error.message(),
1542            format!("more than {MAX_STATEMENTS} statements")
1543        );
1544    }
1545
1546    #[test]
1547    fn key_column_lists_are_bounded() {
1548        // A key may list a column any number of times; each was looked up
1549        // among the table's columns.
1550        let names = vec!["a"; MAX_COLUMNS + 1].join(", ");
1551        for ddl in [
1552            format!("CREATE TABLE t (a INT, PRIMARY KEY ({names}));"),
1553            format!("CREATE TABLE t (a INT, UNIQUE ({names}));"),
1554            format!("CREATE TABLE t (a INT, FOREIGN KEY ({names}) REFERENCES u);"),
1555            format!("CREATE TABLE t (a INT, FOREIGN KEY (a) REFERENCES u ({names}));"),
1556            format!("CREATE TABLE t (a INT REFERENCES u ({names}));"),
1557        ] {
1558            let error = parse(&ddl).unwrap_err();
1559            assert_eq!(
1560                error.to_string(),
1561                format!("line 1: t: a key of more than {MAX_COLUMNS} columns"),
1562            );
1563        }
1564    }
1565
1566    #[test]
1567    fn statements_are_read_one_at_a_time() {
1568        // Every token of the file was held at once: 16 MB of DDL took over
1569        // 2 GB. Now one statement's are, and a statement has a limit.
1570        let long = format!("CREATE TABLE t ({});", "1 ".repeat(MAX_TOKENS));
1571        assert_eq!(
1572            parse(&long).unwrap_err().to_string(),
1573            format!("line 1: a statement of more than {MAX_TOKENS} tokens")
1574        );
1575        let insert = format!(
1576            "INSERT INTO t VALUES ({});\nCREATE TABLE u (a INT);",
1577            "1, ".repeat(MAX_TOKENS)
1578        );
1579        let parsed = parse(&insert).unwrap();
1580        assert_eq!(notes(&parsed), ["line 1: INSERT statement skipped"]);
1581        assert_eq!(parsed.schema.tables()[0].name(), Some("u"));
1582        // Errors keep their order: the text's, then the parentheses', then
1583        // the statements'.
1584        assert_eq!(
1585            parse("CREATE TABLE t (a INT));\nSELECT 'oops")
1586                .unwrap_err()
1587                .to_string(),
1588            "line 2: a string is not closed"
1589        );
1590        assert_eq!(
1591            parse("CREATE TABLE t (a INT REFERENCES);\nx)")
1592                .unwrap_err()
1593                .to_string(),
1594            "line 2: a `)` closes nothing"
1595        );
1596    }
1597
1598    /// PostgreSQL's dollar-quoted strings (a pg_dump's function bodies) were
1599    /// read as SQL: a body's `CREATE TABLE` became a table, its `;` split the
1600    /// function, and an apostrophe in it failed the whole file.
1601    #[test]
1602    fn dollar_quoted_bodies_are_strings() {
1603        let parsed = parse(
1604            "CREATE TABLE users (id INT PRIMARY KEY);\n\
1605             CREATE FUNCTION audit() RETURNS trigger AS $$\n\
1606             BEGIN\n\
1607                 CREATE TEMP TABLE scratch (n INT);\n\
1608                 RETURN NEW;\n\
1609             END;\n\
1610             $$ LANGUAGE plpgsql;\n\
1611             CREATE FUNCTION q() RETURNS text AS $body$ SELECT $q$don't$q$; $body$;\n\
1612             CREATE TABLE posts (id INT, author INT DEFAULT $1);",
1613        )
1614        .unwrap();
1615        let tables: Vec<&str> = parsed
1616            .schema
1617            .tables()
1618            .iter()
1619            .filter_map(Schema::name)
1620            .collect();
1621        assert_eq!(tables, ["users", "posts"]);
1622        assert_eq!(
1623            notes(&parsed),
1624            [
1625                "line 2: CREATE FUNCTION statement skipped",
1626                "line 8: CREATE FUNCTION statement skipped",
1627            ]
1628        );
1629        // `$1` is a parameter, not a quote.
1630        let author = parsed.schema.tables()[1].field("author").unwrap();
1631        assert!(author.default_value().is_some());
1632        assert_eq!(
1633            parse("SELECT $tag$ never closed").unwrap_err().to_string(),
1634            "line 1: a string is not closed"
1635        );
1636    }
1637
1638    /// SQL Server scripts end statements with `GO` lines, not `;`: every
1639    /// table after the first was read as the first one's options and
1640    /// dropped without a note. And their keys say `CLUSTERED`, which failed
1641    /// the file.
1642    #[test]
1643    fn statements_without_semicolons_and_clustered_keys() {
1644        let parsed = parse(
1645            "SET ANSI_NULLS ON\nGO\n\
1646             CREATE TABLE [dbo].[a] ([id] INT NOT NULL,\n\
1647               CONSTRAINT [PK_a] PRIMARY KEY CLUSTERED ([id] ASC) WITH (PAD_INDEX = OFF)\n\
1648             ) ON [PRIMARY]\nGO\n\
1649             CREATE TABLE [dbo].[b] ([a_id] INT REFERENCES [dbo].[a] ([id]))\n\
1650             CREATE TABLE c (x INT)",
1651        )
1652        .unwrap();
1653        let tables: Vec<&str> = parsed
1654            .schema
1655            .tables()
1656            .iter()
1657            .filter_map(Schema::name)
1658            .collect();
1659        assert_eq!(tables, ["dbo.a", "dbo.b", "c"]);
1660        assert_eq!(parsed.schema.tables()[0].primary_key(), ["id"]);
1661        assert_eq!(notes(&parsed), ["line 1: SET statement skipped"]);
1662    }
1663
1664    #[test]
1665    fn odd_input_never_panics() {
1666        for ddl in [
1667            "",
1668            ";;;",
1669            "CREATE",
1670            "CREATE TABLE",
1671            "CREATE TABLE t ()",
1672            "CREATE TABLE t (CONSTRAINT)",
1673            "CREATE TABLE t (a)",
1674            "CREATE TABLE t (a INT, , b INT)",
1675            "CREATE TABLE t (FOREIGN KEY (a) REFERENCES)",
1676            "CREATE TABLE t (UNIQUE)",
1677            "CREATE TABLE t (a INT DEFAULT)",
1678            "CREATE TABLE t (a INT REFERENCES u (",
1679            "CREATE TABLE [t (a INT)",
1680            "CREATE TABLE \"t\" (\"a\"\"b\" INT)",
1681            "ÄÖ ü 1e+ .5 :: ::: \u{0}",
1682        ] {
1683            let _ = parse(ddl);
1684        }
1685        let parsed = parse("CREATE TABLE \"t\" (\"a\"\"b\" INT, c)").unwrap();
1686        let t = &parsed.schema.tables()[0];
1687        assert_eq!(t.fields()[0].name(), "a\"b");
1688        assert_eq!(t.fields()[1].data_type(), &DataType::Unknown);
1689    }
1690}