Skip to main content

quantydb_sqlite/
ddl.rs

1//! Reading a `create table` statement.
2//!
3//! The file says which pages a table lives on, and nothing else about it.
4//! Column names, their order, their declared types, which columns form the
5//! primary key: all of that exists only as the text of the statement that
6//! created the table, kept verbatim in the schema table. So importing
7//! anything means parsing SQLite's dialect of `create table`, and this is
8//! that parser.
9//!
10//! It is not the SQL front end from quantydb-ql and should not become it.
11//! That one parses our dialect, where we decide what is legal. This one
12//! parses text another engine already accepted, including everything we do
13//! not implement: check constraints, foreign key actions, collations,
14//! generated columns, four ways of quoting an identifier, and type names
15//! that are arbitrary words. Bending our own parser around a foreign
16//! grammar would put that mess in the path of every query we run.
17//!
18//! Where it cannot understand something it stops rather than guesses.
19//! A statement SQLite accepted and we cannot read means our parser has a
20//! gap, and a gap that quietly produces a column list of the wrong shape
21//! would be read as data of the wrong shape, which is the failure this
22//! whole crate is built to prevent.
23
24use crate::error::{Result, SqliteError};
25
26/// Whether a column is computed, and if so whether the file holds it.
27///
28/// The distinction decides where every later column's value sits in the
29/// record, so it is a variant rather than a flag: a stored generated column
30/// occupies a slot like any other column, a virtual one occupies none. A
31/// generated column with neither keyword written out is virtual, which was
32/// checked against a file rather than assumed.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Generated {
35    No,
36    Virtual,
37    Stored,
38}
39
40impl Generated {
41    pub fn is_generated(self) -> bool {
42        !matches!(self, Generated::No)
43    }
44
45    /// Whether the file holds no value for this column at all.
46    pub fn is_virtual(self) -> bool {
47        matches!(self, Generated::Virtual)
48    }
49}
50
51/// A column as the create statement declared it.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ColumnDef {
54    pub name: String,
55    /// The declared type, exactly as written, with its arguments: `INTEGER`,
56    /// `NVARCHAR(160)`, `DOUBLE PRECISION`. `None` when the column was
57    /// declared without one, which SQLite allows.
58    pub declared_type: Option<String>,
59    pub not_null: bool,
60    /// The default, as written. Kept as text because a default can be an
61    /// expression, and deciding what a given expression means is a question
62    /// for whoever imports it.
63    pub default: Option<String>,
64    pub generated: Generated,
65}
66
67/// One column of a primary key, in key order.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct KeyColumn {
70    pub name: String,
71    pub descending: bool,
72}
73
74/// A rule the source enforces that we have no equivalent for.
75///
76/// These are recorded, not applied. QuantyDB has keys, indexes and
77/// nullability, and nothing else from this list, so a foreign key or a
78/// check constraint cannot come across. That is a limitation rather than a
79/// choice, and the reason to carry them here at all is that an importer
80/// which does not mention them lets a developer believe their data is still
81/// guarded when it is not.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum Constraint {
84    /// `unique (a, b)`, or a column declared `unique`.
85    Unique { columns: Vec<String> },
86    /// `check (...)`, kept as written.
87    Check { expression: String },
88    /// `foreign key (a) references other (b)`.
89    ForeignKey {
90        columns: Vec<String>,
91        references: String,
92    },
93    /// `collate nocase` on a column, which changes how text compares.
94    Collation { column: String, name: String },
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct TableDef {
99    pub name: String,
100    pub columns: Vec<ColumnDef>,
101    /// Empty when the table has no primary key at all.
102    pub primary_key: Vec<KeyColumn>,
103    pub without_rowid: bool,
104    pub strict: bool,
105    /// Rules the source enforces that we cannot: unique constraints beyond
106    /// the key, check constraints, foreign keys and collations.
107    pub unsupported_constraints: Vec<Constraint>,
108    /// Whether the primary key was written as a column constraint that
109    /// spelled out `desc`. See `rowid_alias` for why that is worth keeping.
110    inline_pk_desc: bool,
111}
112
113impl TableDef {
114    pub fn column(&self, name: &str) -> Option<&ColumnDef> {
115        self.columns
116            .iter()
117            .find(|c| c.name.eq_ignore_ascii_case(name))
118    }
119
120    pub fn column_index(&self, name: &str) -> Option<usize> {
121        self.columns
122            .iter()
123            .position(|c| c.name.eq_ignore_ascii_case(name))
124    }
125
126    /// The column that is an alias for the rowid, if the table has one.
127    ///
128    /// Such a column holds no value of its own in the record; SQLite stores
129    /// NULL there and the value lives in the cell's rowid. Getting this
130    /// wrong means reading the primary key of an entire table as NULL, or
131    /// inventing one, and nothing about it fails loudly, so the rule is
132    /// spelled out here and checked against a fixture built from these
133    /// exact cases:
134    ///
135    /// | declaration                       | alias |
136    /// |-----------------------------------|-------|
137    /// | `x integer primary key`           | yes   |
138    /// | `x integer primary key desc`      | no    |
139    /// | `x integer, primary key (x)`      | yes   |
140    /// | `x integer, primary key (x desc)` | yes   |
141    /// | `x int primary key`               | no    |
142    ///
143    /// The two `desc` rows disagreeing is not a typo. Spelling out `desc`
144    /// in a column constraint suppresses the alias, and spelling it out in
145    /// a table constraint does not. SQLite documents that as a quirk kept
146    /// for backwards compatibility, and every reader has to reproduce it.
147    ///
148    /// A without rowid table has no rowid to alias, so it never has one.
149    pub fn rowid_alias(&self) -> Option<&ColumnDef> {
150        if self.without_rowid || self.primary_key.len() != 1 || self.inline_pk_desc {
151            return None;
152        }
153        let key = &self.primary_key[0];
154        let column = self.column(&key.name)?;
155        match &column.declared_type {
156            // exactly "integer", in any mixture of case. "int" does not
157            // count, and neither does "integer(8)".
158            Some(declared) if declared.trim().eq_ignore_ascii_case("integer") => Some(column),
159            _ => None,
160        }
161    }
162}
163
164// ---------------------------------------------------------------------------
165// tokens
166// ---------------------------------------------------------------------------
167
168#[derive(Debug, Clone, PartialEq, Eq)]
169enum Token {
170    /// A bare word: a keyword, a type name, an unquoted identifier. SQLite
171    /// does not reserve most of these, so which one it is depends on where
172    /// it appears.
173    Word(String),
174    /// An identifier that was quoted, and therefore is never a keyword.
175    Quoted(String),
176    /// A string, number or blob literal, kept as written.
177    Literal(String),
178    Punct(char),
179}
180
181/// A token together with where it sits in the original statement, so that
182/// type names and defaults can be handed back exactly as they were written
183/// rather than reassembled from their parts.
184#[derive(Debug, Clone)]
185struct Tok {
186    token: Token,
187    start: usize,
188    end: usize,
189}
190
191impl Token {
192    /// The text of a word or quoted identifier, for keyword comparison.
193    fn word(&self) -> Option<&str> {
194        match self {
195            Token::Word(w) => Some(w),
196            _ => None,
197        }
198    }
199
200    fn is_word(&self, keyword: &str) -> bool {
201        matches!(self, Token::Word(w) if w.eq_ignore_ascii_case(keyword))
202    }
203
204    fn is_punct(&self, c: char) -> bool {
205        matches!(self, Token::Punct(p) if *p == c)
206    }
207
208    /// The identifier this token names, quoted or not.
209    fn identifier(&self) -> Option<String> {
210        match self {
211            Token::Word(w) => Some(w.clone()),
212            Token::Quoted(q) => Some(q.clone()),
213            _ => None,
214        }
215    }
216
217    /// How this token was written, for reassembling type names and
218    /// defaults.
219    fn text(&self) -> String {
220        match self {
221            Token::Word(w) => w.clone(),
222            Token::Quoted(q) => format!("\"{}\"", q.replace('"', "\"\"")),
223            Token::Literal(l) => l.clone(),
224            Token::Punct(p) => p.to_string(),
225        }
226    }
227}
228
229fn tokenize(sql: &str) -> Result<Vec<Tok>> {
230    let chars: Vec<char> = sql.chars().collect();
231    // char index to byte offset, with a sentinel so that the end of the
232    // last token is addressable
233    let mut offsets: Vec<usize> = sql.char_indices().map(|(i, _)| i).collect();
234    offsets.push(sql.len());
235
236    let mut tokens: Vec<Tok> = Vec::new();
237    let mut at = 0usize;
238
239    while at < chars.len() {
240        let c = chars[at];
241
242        if c.is_whitespace() {
243            at += 1;
244            continue;
245        }
246        let began = at;
247
248        // -- to end of line
249        if c == '-' && chars.get(at + 1) == Some(&'-') {
250            while at < chars.len() && chars[at] != '\n' {
251                at += 1;
252            }
253            continue;
254        }
255
256        // /* ... */, which sqlite allows to run to end of input unclosed
257        if c == '/' && chars.get(at + 1) == Some(&'*') {
258            at += 2;
259            while at < chars.len() && !(chars[at] == '*' && chars.get(at + 1) == Some(&'/')) {
260                at += 1;
261            }
262            at = (at + 2).min(chars.len());
263            continue;
264        }
265
266        // the three quoting styles for identifiers, plus strings
267        if let Some((open, close)) = match c {
268            '"' => Some(('"', '"')),
269            '`' => Some(('`', '`')),
270            '[' => Some(('[', ']')),
271            _ => None,
272        } {
273            at += 1;
274            let mut value = String::new();
275            loop {
276                let Some(&ch) = chars.get(at) else {
277                    return Err(SqliteError::unsupported(format!(
278                        "a create statement has an unterminated {open} quoted name"
279                    )));
280                };
281                at += 1;
282                if ch == close {
283                    // brackets have no escape; the others double the quote
284                    if close != ']' && chars.get(at) == Some(&close) {
285                        value.push(close);
286                        at += 1;
287                        continue;
288                    }
289                    break;
290                }
291                value.push(ch);
292            }
293            tokens.push(Tok {
294                token: Token::Quoted(value),
295                start: offsets[began],
296                end: offsets[at],
297            });
298            continue;
299        }
300
301        if c == '\'' {
302            let mut text = String::from("'");
303            at += 1;
304            loop {
305                let Some(&ch) = chars.get(at) else {
306                    return Err(SqliteError::unsupported(
307                        "a create statement has an unterminated string",
308                    ));
309                };
310                at += 1;
311                text.push(ch);
312                if ch == '\'' {
313                    if chars.get(at) == Some(&'\'') {
314                        text.push('\'');
315                        at += 1;
316                        continue;
317                    }
318                    break;
319                }
320            }
321            tokens.push(Tok {
322                token: Token::Literal(text),
323                start: offsets[began],
324                end: offsets[at],
325            });
326            continue;
327        }
328
329        if c.is_ascii_digit() || (c == '.' && chars.get(at + 1).is_some_and(|d| d.is_ascii_digit()))
330        {
331            let start = at;
332            while at < chars.len()
333                && (chars[at].is_ascii_alphanumeric() || chars[at] == '.' || {
334                    // an exponent's sign belongs to the number
335                    (chars[at] == '+' || chars[at] == '-')
336                        && matches!(chars[at - 1], 'e' | 'E')
337                        && chars[start..at].iter().all(|c| *c != 'x' && *c != 'X')
338                })
339            {
340                at += 1;
341            }
342            tokens.push(Tok {
343                token: Token::Literal(chars[start..at].iter().collect()),
344                start: offsets[began],
345                end: offsets[at],
346            });
347            continue;
348        }
349
350        if c.is_alphabetic() || c == '_' || !c.is_ascii() {
351            let start = at;
352            while at < chars.len()
353                && (chars[at].is_alphanumeric()
354                    || chars[at] == '_'
355                    || chars[at] == '$'
356                    || !chars[at].is_ascii())
357            {
358                at += 1;
359            }
360            let word: String = chars[start..at].iter().collect();
361            // x'00ff' is a blob literal, not the identifier x
362            if (word.eq_ignore_ascii_case("x")) && chars.get(at) == Some(&'\'') {
363                let mut text = word;
364                text.push('\'');
365                at += 1;
366                while let Some(&ch) = chars.get(at) {
367                    at += 1;
368                    text.push(ch);
369                    if ch == '\'' {
370                        break;
371                    }
372                }
373                tokens.push(Tok {
374                    token: Token::Literal(text),
375                    start: offsets[began],
376                    end: offsets[at],
377                });
378            } else {
379                tokens.push(Tok {
380                    token: Token::Word(word),
381                    start: offsets[began],
382                    end: offsets[at],
383                });
384            }
385            continue;
386        }
387
388        tokens.push(Tok {
389            token: Token::Punct(c),
390            start: offsets[began],
391            end: offsets[at + 1],
392        });
393        at += 1;
394    }
395
396    Ok(tokens)
397}
398
399// ---------------------------------------------------------------------------
400// parser
401// ---------------------------------------------------------------------------
402
403/// Words that end a column's type name and begin its constraints.
404const CONSTRAINT_WORDS: &[&str] = &[
405    "constraint",
406    "primary",
407    "not",
408    "null",
409    "unique",
410    "check",
411    "default",
412    "collate",
413    "references",
414    "generated",
415    "as",
416];
417
418struct Parser<'a> {
419    sql: &'a str,
420    tokens: Vec<Tok>,
421    at: usize,
422}
423
424impl<'a> Parser<'a> {
425    fn peek(&self) -> Option<&Token> {
426        self.tokens.get(self.at).map(|t| &t.token)
427    }
428
429    fn at_word(&self, offset: usize, keyword: &str) -> bool {
430        self.tokens
431            .get(self.at + offset)
432            .is_some_and(|t| t.token.is_word(keyword))
433    }
434
435    /// Where the token at `index` starts in the original statement.
436    fn start_of(&self, index: usize) -> usize {
437        match self.tokens.get(index) {
438            Some(t) => t.start,
439            None => self.sql.len(),
440        }
441    }
442
443    /// Where the token before `index` ends.
444    fn end_before(&self, index: usize) -> usize {
445        match index.checked_sub(1).and_then(|i| self.tokens.get(i)) {
446            Some(t) => t.end,
447            None => 0,
448        }
449    }
450
451    /// The original text spanned by the tokens from `from` up to the
452    /// current position, exactly as it was written.
453    fn text_since(&self, from: usize) -> String {
454        let start = self.start_of(from);
455        let end = self.end_before(self.at);
456        if end <= start {
457            String::new()
458        } else {
459            self.sql[start..end].trim().to_string()
460        }
461    }
462
463    fn next(&mut self) -> Option<Token> {
464        let token = self.tokens.get(self.at).map(|t| t.token.clone());
465        if token.is_some() {
466            self.at += 1;
467        }
468        token
469    }
470
471    fn eat_word(&mut self, keyword: &str) -> bool {
472        if self.peek().is_some_and(|t| t.is_word(keyword)) {
473            self.at += 1;
474            true
475        } else {
476            false
477        }
478    }
479
480    fn eat_punct(&mut self, c: char) -> bool {
481        if self.peek().is_some_and(|t| t.is_punct(c)) {
482            self.at += 1;
483            true
484        } else {
485            false
486        }
487    }
488
489    fn expect_word(&mut self, keyword: &str) -> Result<()> {
490        if self.eat_word(keyword) {
491            Ok(())
492        } else {
493            Err(self.gap(&format!("expected {keyword}")))
494        }
495    }
496
497    fn expect_punct(&mut self, c: char) -> Result<()> {
498        if self.eat_punct(c) {
499            Ok(())
500        } else {
501            Err(self.gap(&format!("expected {c}")))
502        }
503    }
504
505    fn identifier(&mut self) -> Result<String> {
506        match self.next().and_then(|t| t.identifier()) {
507            Some(name) => Ok(name),
508            None => Err(self.gap("expected a name")),
509        }
510    }
511
512    /// A statement this parser cannot read. Not `Malformed`: the file is
513    /// fine and SQLite accepted the statement, so this is our gap and the
514    /// message says which token we tripped on.
515    fn gap(&self, what: &str) -> SqliteError {
516        let found = match self.tokens.get(self.at) {
517            Some(t) => format!("{:?}", t.token.text()),
518            None => "the end of the statement".to_string(),
519        };
520        SqliteError::unsupported(format!(
521            "cannot read this create statement: {what}, found {found} at token {}",
522            self.at
523        ))
524    }
525
526    fn at_constraint_word(&self) -> bool {
527        self.peek()
528            .and_then(|t| t.word())
529            .is_some_and(|w| CONSTRAINT_WORDS.iter().any(|k| w.eq_ignore_ascii_case(k)))
530    }
531
532    /// Step over a parenthesised group, returning it exactly as written.
533    /// Assumes the opening paren is next.
534    fn balanced(&mut self) -> Result<String> {
535        let from = self.at;
536        self.expect_punct('(')?;
537        let mut depth = 1usize;
538        while depth > 0 {
539            let Some(token) = self.next() else {
540                return Err(self.gap("expected a closing paren"));
541            };
542            if token.is_punct('(') {
543                depth += 1;
544            } else if token.is_punct(')') {
545                depth -= 1;
546            }
547        }
548        Ok(self.text_since(from))
549    }
550
551    /// A parenthesised list of column names, with the per column extras
552    /// (`collate x`, `asc`, `desc`) stepped over.
553    fn column_list(&mut self) -> Result<Vec<String>> {
554        self.expect_punct('(')?;
555        let mut names = Vec::new();
556        loop {
557            names.push(self.identifier()?);
558            if self.eat_word("collate") {
559                self.identifier()?;
560            }
561            let _ = self.eat_word("asc") || self.eat_word("desc");
562            if self.eat_punct(',') {
563                continue;
564            }
565            self.expect_punct(')')?;
566            return Ok(names);
567        }
568    }
569
570    /// Skip to the next comma or closing paren at the top level, which is
571    /// how clauses we do not interpret are stepped over.
572    fn skip_to_end_of_item(&mut self) -> Result<()> {
573        loop {
574            match self.peek() {
575                None => return Ok(()),
576                Some(t) if t.is_punct(',') || t.is_punct(')') => return Ok(()),
577                Some(t) if t.is_punct('(') => {
578                    self.balanced()?;
579                }
580                _ => {
581                    self.at += 1;
582                }
583            }
584        }
585    }
586
587    /// The keyword after a generated column's expression. Absent means
588    /// virtual, which is sqlite's default and not a guess.
589    fn eat_generated_kind(&mut self) -> Generated {
590        if self.eat_word("stored") {
591            Generated::Stored
592        } else {
593            self.eat_word("virtual");
594            Generated::Virtual
595        }
596    }
597
598    /// `on conflict <action>`, which may follow several constraints.
599    fn eat_conflict_clause(&mut self) {
600        if self.at_word(0, "on") && self.at_word(1, "conflict") {
601            self.at += 2;
602            if self.peek().and_then(|t| t.word()).is_some() {
603                self.at += 1;
604            }
605        }
606    }
607}
608
609/// Parse a `create table` statement into the shape of the table it makes.
610pub fn parse_create_table(sql: &str) -> Result<TableDef> {
611    let tokens = tokenize(sql)?;
612    let mut parser = Parser { sql, tokens, at: 0 };
613
614    parser.expect_word("create")?;
615    // temp tables never appear in a database file's schema, but the
616    // statement is legal and costs one line to accept
617    let _ = parser.eat_word("temp") || parser.eat_word("temporary");
618    parser.expect_word("table")?;
619    if parser.eat_word("if") {
620        parser.expect_word("not")?;
621        parser.expect_word("exists")?;
622    }
623
624    let mut name = parser.identifier()?;
625    if parser.eat_punct('.') {
626        // schema qualified: the part after the dot is the table
627        name = parser.identifier()?;
628    }
629
630    if parser.peek().is_some_and(|t| t.is_word("as")) {
631        return Err(SqliteError::unsupported(format!(
632            "{name} was created by `create table ... as select`, so the file does not record \
633             its column types; this reader cannot import it"
634        )));
635    }
636
637    parser.expect_punct('(')?;
638
639    let mut columns: Vec<ColumnDef> = Vec::new();
640    let mut primary_key: Vec<KeyColumn> = Vec::new();
641    let mut constraints: Vec<Constraint> = Vec::new();
642    let mut inline_pk_desc = false;
643
644    loop {
645        if parser.peek().is_none() {
646            return Err(parser.gap("the column list is not closed"));
647        }
648        if parser.eat_punct(')') {
649            break;
650        }
651
652        // a table constraint, rather than another column
653        let is_table_constraint = {
654            // a table constraint may carry a name, in which case the
655            // keyword that says which kind it is comes two tokens later
656            let offset = if parser.at_word(0, "constraint") {
657                2
658            } else {
659                0
660            };
661            parser.at_word(offset, "primary")
662                || parser.at_word(offset, "unique")
663                || parser.at_word(offset, "check")
664                || parser.at_word(offset, "foreign")
665        };
666
667        if is_table_constraint {
668            if parser.eat_word("constraint") {
669                parser.identifier()?;
670            }
671            if parser.eat_word("primary") {
672                parser.expect_word("key")?;
673                if !primary_key.is_empty() {
674                    return Err(SqliteError::unsupported(format!(
675                        "{name} declares more than one primary key"
676                    )));
677                }
678                parser.expect_punct('(')?;
679                loop {
680                    let column = parser.identifier()?;
681                    if parser.eat_word("collate") {
682                        parser.identifier()?;
683                    }
684                    let descending = if parser.eat_word("desc") {
685                        true
686                    } else {
687                        parser.eat_word("asc");
688                        false
689                    };
690                    primary_key.push(KeyColumn {
691                        name: column,
692                        descending,
693                    });
694                    if parser.eat_punct(',') {
695                        continue;
696                    }
697                    parser.expect_punct(')')?;
698                    break;
699                }
700                parser.eat_conflict_clause();
701            } else if parser.eat_word("unique") {
702                let columns = parser.column_list()?;
703                constraints.push(Constraint::Unique { columns });
704                parser.eat_conflict_clause();
705                parser.skip_to_end_of_item()?;
706            } else if parser.eat_word("check") {
707                let expression = parser.balanced()?;
708                constraints.push(Constraint::Check { expression });
709                parser.skip_to_end_of_item()?;
710            } else if parser.eat_word("foreign") {
711                parser.expect_word("key")?;
712                let columns = parser.column_list()?;
713                let references = if parser.eat_word("references") {
714                    parser.identifier()?
715                } else {
716                    String::new()
717                };
718                constraints.push(Constraint::ForeignKey {
719                    columns,
720                    references,
721                });
722                parser.skip_to_end_of_item()?;
723            } else {
724                parser.skip_to_end_of_item()?;
725            }
726        } else {
727            let column_name = parser.identifier()?;
728            let type_from = parser.at;
729            while !parser.at_constraint_word() {
730                match parser.peek() {
731                    None => break,
732                    Some(t) if t.is_punct(',') || t.is_punct(')') => break,
733                    Some(t) if t.is_punct('(') => {
734                        parser.balanced()?;
735                    }
736                    Some(_) => {
737                        parser.at += 1;
738                    }
739                }
740            }
741            let declared_type = parser.text_since(type_from);
742
743            let mut column = ColumnDef {
744                name: column_name,
745                declared_type: if declared_type.is_empty() {
746                    None
747                } else {
748                    Some(declared_type)
749                },
750                not_null: false,
751                default: None,
752                generated: Generated::No,
753            };
754
755            // column constraints, in any order and any number
756            loop {
757                if parser.eat_word("constraint") {
758                    parser.identifier()?;
759                    continue;
760                }
761                if parser.eat_word("primary") {
762                    parser.expect_word("key")?;
763                    if !primary_key.is_empty() {
764                        return Err(SqliteError::unsupported(format!(
765                            "{name} declares more than one primary key"
766                        )));
767                    }
768                    let descending = if parser.eat_word("desc") {
769                        true
770                    } else {
771                        parser.eat_word("asc");
772                        false
773                    };
774                    inline_pk_desc = descending;
775                    primary_key.push(KeyColumn {
776                        name: column.name.clone(),
777                        descending,
778                    });
779                    parser.eat_conflict_clause();
780                    parser.eat_word("autoincrement");
781                    continue;
782                }
783                if parser.eat_word("not") {
784                    parser.expect_word("null")?;
785                    column.not_null = true;
786                    parser.eat_conflict_clause();
787                    continue;
788                }
789                if parser.eat_word("null") {
790                    parser.eat_conflict_clause();
791                    continue;
792                }
793                if parser.eat_word("unique") {
794                    constraints.push(Constraint::Unique {
795                        columns: vec![column.name.clone()],
796                    });
797                    parser.eat_conflict_clause();
798                    continue;
799                }
800                if parser.eat_word("check") {
801                    let expression = parser.balanced()?;
802                    constraints.push(Constraint::Check { expression });
803                    continue;
804                }
805                if parser.eat_word("default") {
806                    let from = parser.at;
807                    if parser.peek().is_some_and(|t| t.is_punct('(')) {
808                        parser.balanced()?;
809                    } else {
810                        // a literal, a keyword like current_timestamp, or a
811                        // signed number
812                        let _ = parser.eat_punct('-') || parser.eat_punct('+');
813                        if parser.next().is_none() {
814                            return Err(parser.gap("expected a default value"));
815                        }
816                    }
817                    column.default = Some(parser.text_since(from));
818                    continue;
819                }
820                if parser.eat_word("collate") {
821                    let name = parser.identifier()?;
822                    // binary is sqlite's default and changes nothing
823                    if !name.eq_ignore_ascii_case("binary") {
824                        constraints.push(Constraint::Collation {
825                            column: column.name.clone(),
826                            name,
827                        });
828                    }
829                    continue;
830                }
831                if parser.eat_word("references") {
832                    let references = parser.identifier().unwrap_or_default();
833                    constraints.push(Constraint::ForeignKey {
834                        columns: vec![column.name.clone()],
835                        references,
836                    });
837                    parser.skip_to_end_of_item()?;
838                    continue;
839                }
840                // `generated always as (...)` and the shorthand `as (...)`
841                // mean the same thing, and neither keyword after the
842                // expression means virtual
843                if parser.eat_word("generated") {
844                    parser.expect_word("always")?;
845                    parser.expect_word("as")?;
846                    parser.balanced()?;
847                    column.generated = parser.eat_generated_kind();
848                    continue;
849                }
850                if parser.peek().is_some_and(|t| t.is_word("as")) {
851                    parser.at += 1;
852                    parser.balanced()?;
853                    column.generated = parser.eat_generated_kind();
854                    continue;
855                }
856                break;
857            }
858
859            columns.push(column);
860        }
861
862        if parser.eat_punct(',') {
863            continue;
864        }
865        parser.expect_punct(')')?;
866        break;
867    }
868
869    // table options: without rowid and strict, in either order
870    let mut without_rowid = false;
871    let mut strict = false;
872    loop {
873        if parser.eat_word("without") {
874            parser.expect_word("rowid")?;
875            without_rowid = true;
876        } else if parser.eat_word("strict") {
877            strict = true;
878        } else {
879            break;
880        }
881        if !parser.eat_punct(',') {
882            break;
883        }
884    }
885    let _ = parser.eat_punct(';');
886
887    if columns.is_empty() {
888        return Err(SqliteError::unsupported(format!(
889            "{name} was parsed as having no columns"
890        )));
891    }
892    for key in &primary_key {
893        if !columns
894            .iter()
895            .any(|c| c.name.eq_ignore_ascii_case(&key.name))
896        {
897            return Err(SqliteError::unsupported(format!(
898                "{name} has a primary key over {}, which is not one of its columns",
899                key.name
900            )));
901        }
902    }
903
904    Ok(TableDef {
905        name,
906        columns,
907        primary_key,
908        without_rowid,
909        strict,
910        unsupported_constraints: constraints,
911        inline_pk_desc,
912    })
913}