klukai_types/
schema.rs

1#![allow(clippy::result_large_err)]
2
3use std::{
4    collections::{HashMap, HashSet},
5    fmt,
6    time::{Instant, SystemTime},
7};
8
9use enquote::unquote;
10use fallible_iterator::FallibleIterator;
11use indexmap::{IndexMap, IndexSet};
12use rusqlite::{Connection, Transaction};
13use serde::{Deserialize, Serialize};
14use sqlite3_parser::ast::{
15    Cmd, ColumnConstraint, ColumnDefinition, CreateTableBody, Expr, Name, NamedTableConstraint,
16    QualifiedName, SortedColumn, Stmt, TableConstraint, fmt::ToTokens,
17};
18use tracing::{debug, info, trace};
19
20#[derive(Debug, Clone, Eq, PartialEq)]
21pub struct Column {
22    pub name: String,
23    pub sql_type: (SqliteType, Option<String>),
24    pub nullable: bool,
25    pub default_value: Option<String>,
26    pub generated: Option<Generated>,
27    pub primary_key: bool,
28    pub raw: ColumnDefinition,
29}
30
31#[derive(Debug, Clone, Eq, PartialEq, Hash)]
32pub struct Generated {
33    pub raw: String,
34    pub from: Vec<String>,
35}
36
37impl Column {
38    pub fn sql_type(&self) -> (SqliteType, Option<&str>) {
39        (self.sql_type.0, self.sql_type.1.as_deref())
40    }
41}
42
43impl std::hash::Hash for Column {
44    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
45        self.name.hash(state);
46        self.sql_type.hash(state);
47        self.nullable.hash(state);
48        self.default_value.hash(state);
49        self.generated.hash(state);
50        self.primary_key.hash(state);
51    }
52}
53
54impl fmt::Display for Column {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        self.raw.to_fmt(f)
57    }
58}
59
60/// SQLite data types.
61/// See [Fundamental Datatypes](https://sqlite.org/c3ref/c_blob.html).
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum SqliteType {
65    /// NULL
66    Null,
67    /// String
68    Text,
69    /// Integer or Real (default affinity)
70    Numeric,
71    /// 64-bit signed integer
72    Integer,
73    /// 64-bit IEEE floating point number
74    Real,
75    /// BLOB
76    Blob,
77}
78
79#[derive(Debug, Clone)]
80pub struct Table {
81    pub name: String,
82    pub pk: IndexSet<String>,
83    pub columns: IndexMap<String, Column>,
84    pub indexes: IndexMap<String, Index>,
85    pub raw: CreateTableBody,
86}
87
88impl fmt::Display for Table {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        Cmd::Stmt(Stmt::CreateTable {
91            temporary: false,
92            if_not_exists: false,
93            tbl_name: QualifiedName::single(Name(self.name.as_str().into())),
94            body: self.raw.clone(),
95        })
96        .to_fmt(f)
97    }
98}
99
100#[derive(Debug, Clone, Eq, PartialEq)]
101pub struct Index {
102    pub name: String,
103    pub tbl_name: String,
104    pub columns: Vec<SortedColumn>,
105    pub where_clause: Option<Expr>,
106    pub unique: bool,
107}
108
109#[derive(Debug, Clone, Default)]
110pub struct Schema {
111    pub tables: IndexMap<String, Table>,
112}
113
114impl Schema {
115    pub fn constrain(&mut self) -> Result<(), ConstrainedSchemaError> {
116        self.tables.retain(|name, _table| {
117            !(name.contains("crsql") && name.contains("sqlite") && name.starts_with("__corro"))
118        });
119
120        for (tbl_name, table) in self.tables.iter() {
121            // this should always be the case...
122            if let CreateTableBody::ColumnsAndConstraints {
123                columns: _,
124                constraints,
125                ..
126            } = &table.raw
127            {
128                if let Some(constraints) = constraints {
129                    for named in constraints.iter() {
130                        if let TableConstraint::PrimaryKey { columns, .. } = &named.constraint {
131                            for column in columns.iter() {
132                                if !matches!(column.expr, Expr::Id(_)) {
133                                    return Err(ConstrainedSchemaError::PrimaryKeyExpr);
134                                }
135                            }
136                        }
137                    }
138                }
139            } else {
140                // error here!
141            }
142
143            for (name, column) in table.columns.iter() {
144                if !column.primary_key && !column.nullable && column.default_value.is_none() {
145                    return Err(ConstrainedSchemaError::NotNullableColumnNeedsDefault {
146                        tbl_name: tbl_name.clone(),
147                        name: name.clone(),
148                    });
149                }
150
151                if column
152                    .raw
153                    .constraints
154                    .iter()
155                    .any(|named| matches!(named.constraint, ColumnConstraint::ForeignKey { .. }))
156                {
157                    return Err(ConstrainedSchemaError::ForeignKey {
158                        tbl_name: tbl_name.clone(),
159                        name: name.clone(),
160                    });
161                }
162            }
163
164            for (name, index) in table.indexes.iter() {
165                if index.unique {
166                    return Err(ConstrainedSchemaError::UniqueIndex(name.clone()));
167                }
168            }
169        }
170
171        Ok(())
172    }
173}
174
175#[derive(Debug, thiserror::Error)]
176pub enum SchemaError {
177    #[error(transparent)]
178    Io(#[from] std::io::Error),
179    #[error(transparent)]
180    Rusqlite(#[from] rusqlite::Error),
181    #[error(transparent)]
182    Parse(#[from] sqlite3_parser::lexer::sql::Error),
183    #[error("nothing to parse")]
184    NothingParsed,
185    #[error("unsupported command: {0}")]
186    UnsupportedCmd(Cmd),
187    #[error("missing table for index (table: '{tbl_name}', index: '{name}')")]
188    IndexWithoutTable { tbl_name: String, name: String },
189    #[error("temporary tables are not supported: {0}")]
190    TemporaryTable(Cmd),
191}
192
193#[derive(Debug, thiserror::Error)]
194#[allow(clippy::large_enum_variant)]
195pub enum ConstrainedSchemaError {
196    #[error("unique indexes are not supported: {0}")]
197    UniqueIndex(String),
198    #[error("table as select arenot supported: {0}")]
199    TableAsSelect(Cmd),
200    #[error(
201        "not nullable column '{name}' on table '{tbl_name}' needs a default value for forward schema compatibility"
202    )]
203    NotNullableColumnNeedsDefault { tbl_name: String, name: String },
204    #[error("foreign keys are not supported (table: '{tbl_name}', column: '{name}')")]
205    ForeignKey { tbl_name: String, name: String },
206    #[error("expr used as primary")]
207    PrimaryKeyExpr,
208}
209
210#[allow(clippy::result_large_err)]
211pub fn init_schema(conn: &Connection) -> Result<Schema, SchemaError> {
212    let mut dump = String::new();
213
214    let tables: HashMap<String, String> = conn
215        .prepare(r#"SELECT name, sql FROM __corro_schema WHERE type = "table" ORDER BY tbl_name"#)?
216        .query_map((), |row| {
217            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
218        })?
219        .collect::<rusqlite::Result<_>>()?;
220
221    for sql in tables.values() {
222        dump.push_str(sql.as_str());
223        dump.push(';');
224    }
225
226    let indexes: HashMap<String, String> = conn
227        .prepare(r#"SELECT name, sql FROM __corro_schema WHERE type = "index" ORDER BY tbl_name"#)?
228        .query_map((), |row| {
229            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
230        })?
231        .collect::<rusqlite::Result<_>>()?;
232
233    for sql in indexes.values() {
234        dump.push_str(sql.as_str());
235        dump.push(';');
236    }
237
238    parse_sql(dump.as_str())
239}
240
241#[derive(Debug, thiserror::Error)]
242pub enum ApplySchemaError {
243    #[error(transparent)]
244    Rusqlite(#[from] rusqlite::Error),
245    #[error(transparent)]
246    Schema(#[from] SchemaError),
247    #[error(transparent)]
248    ConstrainedSchema(#[from] ConstrainedSchemaError),
249    #[error("won't drop table without the destructive flag set (table: '{0}')")]
250    DropTableWithoutDestructiveFlag(String),
251    #[error("won't remove column without the destructive flag set (table: '{0}', column: '{1}')")]
252    RemoveColumnWithoutDestructiveFlag(String, String),
253    #[error("won't change column without the destructive flag set (table: '{0}', column: '{1}')")]
254    ChangeColumnWithoutDestructiveFlag(String, String),
255    #[error("can't add a primary key (table: '{0}', columns: '{1}')")]
256    AddPrimaryKey(String, String),
257    #[error("can't modify primary keys (table: '{0}')")]
258    ModifyPrimaryKeys(String),
259
260    #[error(
261        "tried importing an existing schema for table '{0}' due to a failed CREATE TABLE but didn't find anything (this should never happen)"
262    )]
263    ImportedSchemaNotFound(String),
264
265    #[error(
266        "existing schema for table '{tbl_name}' primary keys mismatched, expected: {expected:?}, got: {got:?}"
267    )]
268    ImportedSchemaPkMismatch {
269        tbl_name: String,
270        expected: IndexSet<String>,
271        got: IndexSet<String>,
272    },
273
274    #[error(
275        "existing schema for table '{tbl_name}' columns mismatched, expected: {expected:?}, got: {got:?}"
276    )]
277    ImportedSchemaColumnsMismatch {
278        tbl_name: String,
279        expected: IndexMap<String, Column>,
280        got: IndexMap<String, Column>,
281    },
282}
283
284#[allow(clippy::result_large_err)]
285pub fn apply_schema(
286    tx: &Transaction,
287    schema: &Schema,
288    new_schema: &mut Schema,
289) -> Result<(), ApplySchemaError> {
290    if let Some(name) = schema
291        .tables
292        .keys()
293        .collect::<HashSet<_>>()
294        .difference(&new_schema.tables.keys().collect::<HashSet<_>>())
295        .next()
296    {
297        // TODO: add options and check flag
298        return Err(ApplySchemaError::DropTableWithoutDestructiveFlag(
299            (*name).clone(),
300        ));
301    }
302
303    let mut schema_to_merge = Schema::default();
304
305    {
306        let new_table_names = new_schema
307            .tables
308            .keys()
309            .collect::<HashSet<_>>()
310            .difference(&schema.tables.keys().collect::<HashSet<_>>())
311            .cloned()
312            .collect::<HashSet<_>>();
313
314        debug!("new table names: {new_table_names:?}");
315
316        let new_tables_iter = new_schema
317            .tables
318            .iter()
319            .filter(|(table, _)| new_table_names.contains(table));
320
321        for (name, table) in new_tables_iter {
322            info!("creating table '{name}'");
323            let create_table_res = tx.execute_batch(
324                &Cmd::Stmt(Stmt::CreateTable {
325                    temporary: false,
326                    if_not_exists: false,
327                    tbl_name: QualifiedName::single(Name(name.to_string().into())),
328                    body: table.raw.clone(),
329                })
330                .to_string(),
331            );
332
333            if let Err(e) = create_table_res {
334                debug!(
335                    "could not create table '{name}', trying to reconcile schema if table already exists"
336                );
337                let sql: Vec<String> = tx
338                .prepare(
339                    "SELECT sql FROM sqlite_schema WHERE tbl_name = ? AND type IN ('table', 'index') AND name IS NOT NULL AND sql IS NOT NULL")?.query_map(
340                    [name],
341                    |row| row.get(0),
342                )?.collect::<rusqlite::Result<Vec<_>>>()?;
343
344                if sql.is_empty() {
345                    return Err(e.into());
346                }
347
348                let sql = sql.join(";");
349                info!("found existing schema for '{name}'");
350
351                let parsed_table = parse_sql(&sql)?
352                    .tables
353                    .swap_remove(name)
354                    .ok_or_else(|| ApplySchemaError::ImportedSchemaNotFound(name.clone()))?;
355
356                if parsed_table.pk != table.pk {
357                    return Err(ApplySchemaError::ImportedSchemaPkMismatch {
358                        tbl_name: name.clone(),
359                        expected: table.pk.clone(),
360                        got: parsed_table.pk,
361                    });
362                }
363
364                if parsed_table.columns != table.columns {
365                    return Err(ApplySchemaError::ImportedSchemaColumnsMismatch {
366                        tbl_name: name.clone(),
367                        expected: table.columns.clone(),
368                        got: parsed_table.columns,
369                    });
370                }
371
372                schema_to_merge.tables.insert(name.clone(), parsed_table);
373            }
374
375            debug!("selecting crsql_as_crr");
376            tx.execute_batch(&format!("SELECT crsql_as_crr('{name}'); CREATE INDEX IF NOT EXISTS corro_{name}__crsql_clock_site_id_dbv ON {name}__crsql_clock (site_id, db_version);"))?;
377            debug!("done selecting as crr");
378
379            if schema_to_merge.tables.contains_key(name) {
380                // just merged!
381                continue;
382            }
383
384            for (idx_name, index) in table.indexes.iter() {
385                info!("creating index '{idx_name}'");
386                tx.execute_batch(
387                    &Cmd::Stmt(Stmt::CreateIndex {
388                        unique: false,
389                        if_not_exists: false,
390                        idx_name: QualifiedName::single(Name(idx_name.clone().into())),
391                        tbl_name: Name(index.tbl_name.clone().into()),
392                        columns: index.columns.clone(),
393                        where_clause: index.where_clause.clone(),
394                    })
395                    .to_string(),
396                )?;
397            }
398        }
399    }
400
401    for (name, table) in schema_to_merge.tables {
402        new_schema.tables.insert(name, table);
403    }
404
405    // iterate intersecting tables
406    for name in new_schema
407        .tables
408        .keys()
409        .collect::<HashSet<_>>()
410        .intersection(&schema.tables.keys().collect::<HashSet<_>>())
411        .cloned()
412    {
413        debug!("processing table '{name}'");
414        let table = schema.tables.get(name).unwrap();
415        debug!(
416            "current cols: {:?}",
417            table.columns.keys().collect::<Vec<&String>>()
418        );
419        let new_table = new_schema.tables.get(name).unwrap();
420        debug!(
421            "new cols: {:?}",
422            new_table.columns.keys().collect::<Vec<&String>>()
423        );
424
425        // 1. Check column drops... don't allow unless flag is passed
426
427        let dropped_cols = table
428            .columns
429            .keys()
430            .collect::<HashSet<_>>()
431            .difference(&new_table.columns.keys().collect::<HashSet<_>>())
432            .cloned()
433            .collect::<HashSet<_>>();
434
435        debug!("dropped cols: {dropped_cols:?}");
436
437        if let Some(col_name) = dropped_cols.into_iter().next() {
438            return Err(ApplySchemaError::RemoveColumnWithoutDestructiveFlag(
439                name.clone(),
440                col_name.clone(),
441            ));
442        }
443
444        // 2. check for changed columns
445
446        let changed_cols: HashMap<String, Column> = table
447            .columns
448            .iter()
449            .filter_map(|(name, col)| {
450                new_table
451                    .columns
452                    .get(name)
453                    .and_then(|new_col| (new_col != col).then(|| (name.clone(), col.clone())))
454            })
455            .collect();
456
457        debug!(
458            "changed cols: {:?}",
459            changed_cols.keys().collect::<Vec<_>>()
460        );
461
462        if !changed_cols.is_empty() {
463            // TODO: add destructive flag
464            return Err(ApplySchemaError::ChangeColumnWithoutDestructiveFlag(
465                table.name.clone(),
466                changed_cols.keys().cloned().collect::<Vec<_>>().join(","),
467            ));
468        }
469
470        let new_col_names = new_table
471            .columns
472            .keys()
473            .collect::<HashSet<_>>()
474            .difference(&table.columns.keys().collect::<HashSet<_>>())
475            .cloned()
476            .collect::<HashSet<_>>();
477
478        info!("new columns: {new_col_names:?}");
479
480        if changed_cols.is_empty() {
481            // 2.1. no changed columns, add missing ones
482
483            if new_col_names.is_empty() {
484                // nothing to do
485            } else {
486                info!("Altering crsql for table {}", table.name);
487                let start = Instant::now();
488                let new_cols = new_table
489                    .columns
490                    .iter()
491                    .filter(|(col_name, _)| new_col_names.contains(col_name))
492                    .collect::<Vec<_>>();
493
494                // if all columns are generated, we don't need a migration
495                let require_migration = !new_cols.iter().all(|(_, col)| col.generated.is_some());
496
497                if require_migration {
498                    tx.execute_batch(&format!("SELECT crsql_begin_alter('{name}');"))?;
499                }
500
501                for (col_name, col) in new_cols {
502                    info!("adding column '{col_name}'");
503                    if col.primary_key {
504                        return Err(ApplySchemaError::AddPrimaryKey(
505                            name.clone(),
506                            col_name.clone(),
507                        ));
508                    }
509                    if !col.nullable && col.default_value.is_none() {
510                        return Err(ConstrainedSchemaError::NotNullableColumnNeedsDefault {
511                            tbl_name: name.clone(),
512                            name: col_name.clone(),
513                        }
514                        .into());
515                    }
516                    tx.execute_batch(&format!("ALTER TABLE {name} ADD COLUMN {}", col))?;
517                }
518
519                if require_migration {
520                    tx.execute_batch(&format!("SELECT crsql_commit_alter('main', '{name}', 1);"))?;
521                }
522                info!(
523                    "Altering crsql for table {} took {:?}",
524                    table.name,
525                    start.elapsed()
526                );
527            }
528        } else {
529            // 2.2 we do have changed columns, try to do something about that
530
531            info!("Columns have changed... replacing table {}", table.name);
532            let start = Instant::now();
533
534            let primary_keys = table
535                .columns
536                .values()
537                .filter_map(|col| col.primary_key.then_some(&col.name))
538                .collect::<Vec<&String>>();
539
540            let new_primary_keys = new_table
541                .columns
542                .values()
543                .filter_map(|col| col.primary_key.then_some(&col.name))
544                .collect::<Vec<&String>>();
545
546            if primary_keys != new_primary_keys {
547                return Err(ApplySchemaError::ModifyPrimaryKeys(name.clone()));
548            }
549
550            // "12-step" process to modifying a table
551
552            // first, create our new table with a temp name
553            let tmp_name = format!(
554                "{name}_{}",
555                SystemTime::now()
556                    .duration_since(SystemTime::UNIX_EPOCH)
557                    .unwrap()
558                    .as_millis()
559            );
560
561            let create_tmp_table = Cmd::Stmt(Stmt::CreateTable {
562                temporary: false,
563                if_not_exists: false,
564                tbl_name: QualifiedName::single(Name(tmp_name.clone().into())),
565                body: new_table.raw.clone(),
566            });
567
568            tx.execute_batch("SELECT crsql_begin_alter('{name}');")?;
569
570            info!("creating tmp table '{tmp_name}'");
571            tx.execute_batch(&create_tmp_table.to_string())?;
572
573            let col_names = table
574                .columns
575                .keys()
576                .cloned()
577                .collect::<Vec<String>>()
578                .join(",");
579
580            info!("inserting data from '{name}' into '{tmp_name}'");
581            let inserted = tx.execute(
582                &format!("INSERT INTO {tmp_name} ({col_names}) SELECT {col_names} FROM {name}"),
583                (),
584            )?;
585
586            info!("re-inserted {inserted} rows into the new table for {name}");
587
588            info!("dropping old table '{name}', renaming '{tmp_name}' to '{name}'");
589            tx.execute_batch(&format!(
590                "DROP TABLE {name};
591                 ALTER TABLE {tmp_name} RENAME TO {name}"
592            ))?;
593
594            tx.execute_batch(&format!("SELECT crsql_commit_alter('{name}');"))?;
595            info!("Replacing table {} took {:?}", table.name, start.elapsed());
596        }
597
598        let new_index_names = new_table
599            .indexes
600            .keys()
601            .collect::<HashSet<_>>()
602            .difference(&table.indexes.keys().collect::<HashSet<_>>())
603            .cloned()
604            .collect::<HashSet<_>>();
605
606        let new_indexes_iter = new_table
607            .indexes
608            .iter()
609            .filter(|(index, _)| new_index_names.contains(index));
610
611        for (idx_name, index) in new_indexes_iter {
612            info!("creating new index '{idx_name}'");
613            tx.execute_batch(
614                &Cmd::Stmt(Stmt::CreateIndex {
615                    unique: false,
616                    if_not_exists: false,
617                    idx_name: QualifiedName::single(Name(idx_name.clone().into())),
618                    tbl_name: Name(index.tbl_name.clone().into()),
619                    columns: index.columns.clone(),
620                    where_clause: index.where_clause.clone(),
621                })
622                .to_string(),
623            )?;
624        }
625
626        let dropped_indexes = table
627            .indexes
628            .keys()
629            .collect::<HashSet<_>>()
630            .difference(&new_table.indexes.keys().collect::<HashSet<_>>())
631            .cloned()
632            .collect::<HashSet<_>>();
633
634        for idx_name in dropped_indexes {
635            info!("dropping index '{idx_name}'");
636            tx.execute_batch(&format!("DROP INDEX {idx_name}"))?;
637        }
638
639        let changed_indexes_iter = table.indexes.iter().filter_map(|(idx_name, index)| {
640            let pindex = new_table.indexes.get(idx_name)?;
641            if pindex != index {
642                Some((idx_name, pindex))
643            } else {
644                None
645            }
646        });
647
648        for (idx_name, index) in changed_indexes_iter {
649            info!("replacing index '{idx_name}' (drop + create)");
650            tx.execute_batch(&format!(
651                "DROP INDEX {idx_name}; {}",
652                &Cmd::Stmt(Stmt::CreateIndex {
653                    unique: false,
654                    if_not_exists: false,
655                    idx_name: QualifiedName::single(Name(idx_name.clone().into())),
656                    tbl_name: Name(index.tbl_name.clone().into()),
657                    columns: index.columns.clone(),
658                    where_clause: index.where_clause.clone(),
659                })
660                .to_string(),
661            ))?;
662        }
663    }
664
665    Ok(())
666}
667
668#[allow(clippy::result_large_err)]
669pub fn parse_sql_to_schema(schema: &mut Schema, sql: &str) -> Result<(), SchemaError> {
670    trace!("parsing {sql}");
671    let mut parser = sqlite3_parser::lexer::sql::Parser::new(sql.as_bytes());
672
673    loop {
674        match parser.next() {
675            Ok(None) => break,
676            Err(err) => {
677                eprintln!("Err: {err}");
678                return Err(err.into());
679            }
680            Ok(Some(ref cmd @ Cmd::Stmt(ref stmt))) => match stmt {
681                Stmt::CreateTable {
682                    temporary: true, ..
683                } => return Err(SchemaError::TemporaryTable(cmd.clone())),
684                Stmt::CreateTable {
685                    body: CreateTableBody::AsSelect(_),
686                    ..
687                } => return Err(SchemaError::TemporaryTable(cmd.clone())),
688                Stmt::CreateTable {
689                    temporary: false,
690                    if_not_exists: _,
691                    tbl_name,
692                    body:
693                        CreateTableBody::ColumnsAndConstraints {
694                            columns,
695                            constraints,
696                            ..
697                        },
698                } => {
699                    let table = prepare_table(
700                        tbl_name,
701                        &columns.values().cloned().collect::<Vec<_>>(),
702                        constraints.as_ref(),
703                    );
704                    schema.tables.insert(table.name.clone(), table);
705                    trace!("inserted table: {}", tbl_name.name.0);
706                }
707                Stmt::CreateIndex {
708                    unique,
709                    idx_name,
710                    tbl_name,
711                    columns,
712                    where_clause,
713                    ..
714                } => {
715                    let tbl_name = unquote(&tbl_name.0).unwrap_or_else(|_| tbl_name.0.to_string());
716                    let idx_name =
717                        unquote(&idx_name.name.0).unwrap_or_else(|_| idx_name.name.0.to_string());
718                    if let Some(table) = schema.tables.get_mut(tbl_name.as_str()) {
719                        table.indexes.insert(
720                            idx_name.clone(),
721                            Index {
722                                name: idx_name,
723                                tbl_name,
724                                columns: columns.to_vec(),
725                                where_clause: where_clause.clone(),
726                                unique: *unique,
727                            },
728                        );
729                    } else {
730                        return Err(SchemaError::IndexWithoutTable {
731                            tbl_name: tbl_name.clone(),
732                            name: idx_name.clone(),
733                        });
734                    }
735                }
736                _ => return Err(SchemaError::UnsupportedCmd(cmd.clone())),
737            },
738            Ok(Some(cmd)) => return Err(SchemaError::UnsupportedCmd(cmd)),
739        }
740    }
741
742    Ok(())
743}
744
745#[allow(clippy::result_large_err)]
746pub fn parse_sql(sql: &str) -> Result<Schema, SchemaError> {
747    let mut schema = Schema::default();
748
749    parse_sql_to_schema(&mut schema, sql)?;
750
751    Ok(schema)
752}
753
754#[allow(clippy::result_large_err)]
755fn prepare_table(
756    tbl_name: &QualifiedName,
757    columns: &[ColumnDefinition],
758    constraints: Option<&Vec<NamedTableConstraint>>,
759) -> Table {
760    let pk = constraints
761        .and_then(|constraints| {
762            constraints
763                .iter()
764                .find_map(|named| match &named.constraint {
765                    TableConstraint::PrimaryKey { columns, .. } => Some(
766                        columns
767                            .iter()
768                            .filter_map(|col| match &col.expr {
769                                Expr::Id(id) => {
770                                    Some(unquote(&id.0).unwrap_or_else(|_| id.0.to_string()))
771                                }
772                                _ => None,
773                            })
774                            .collect::<IndexSet<_>>(),
775                    ),
776                    _ => None,
777                })
778        })
779        .unwrap_or_else(|| {
780            columns
781                .iter()
782                .filter(|&def| {
783                    def.constraints.iter().any(|named| {
784                        matches!(named.constraint, ColumnConstraint::PrimaryKey { .. })
785                    })
786                })
787                .map(|def| unquote(&def.col_name.0).unwrap_or_else(|_| def.col_name.0.to_string()))
788                .collect()
789        });
790
791    Table {
792        name: unquote(&tbl_name.name.0).unwrap_or_else(|_| tbl_name.name.0.to_string()),
793        indexes: IndexMap::new(),
794        columns: columns
795            .iter()
796            .map(|def| {
797                trace!("visiting column: {}", def.col_name.0);
798                let default_value = def.constraints.iter().find_map(|named| {
799                    if let ColumnConstraint::Default(ref expr) = named.constraint {
800                        Some(expr.to_string())
801                    } else {
802                        None
803                    }
804                });
805
806                let not_nullable = def.constraints.iter().any(|named| {
807                    matches!(
808                        named.constraint,
809                        ColumnConstraint::NotNull {
810                            nullable: false,
811                            ..
812                        }
813                    )
814                });
815                let nullable = !not_nullable;
816
817                let primary_key = pk.contains(def.col_name.0.as_ref());
818
819                let col_name =
820                    unquote(&def.col_name.0).unwrap_or_else(|_| def.col_name.0.to_string());
821
822                (
823                    col_name.clone(),
824                    Column {
825                        name: col_name.clone(),
826                        sql_type: match def.col_type.as_ref().map(|t| t.name.to_ascii_uppercase()) {
827                            // 1. If the declared type contains the string "INT" then it is assigned INTEGER affinity.
828                            Some(s) if s.contains("INT") => (SqliteType::Integer, Some(s)),
829                            // 2. If the declared type of the column contains any of the strings "CHAR", "CLOB", or "TEXT" then that column has TEXT affinity. Notice that the type VARCHAR contains the string "CHAR" and is thus assigned TEXT affinity.
830                            Some(s)
831                                if s.contains("CHAR")
832                                    || s.contains("CLOB")
833                                    || s.contains("TEXT")
834                                    || s == "JSON" =>
835                            {
836                                (SqliteType::Text, Some(s))
837                            }
838
839                            // 3. If the declared type for a column contains the string "BLOB" or if no type is specified then the column has affinity BLOB.
840                            Some(s) if s.contains("BLOB") || s == "JSONB" => {
841                                (SqliteType::Blob, Some(s))
842                            }
843                            None => (SqliteType::Blob, None),
844
845                            // 4. If the declared type for a column contains any of the strings "REAL", "FLOA", or "DOUB" then the column has REAL affinity.
846                            Some(s)
847                                if s.contains("REAL")
848                                    || s.contains("FLOA")
849                                    || s.contains("DOUB")
850                                    || s == "ANY" =>
851                            {
852                                (SqliteType::Real, Some(s))
853                            }
854
855                            // 5. Otherwise, the affinity is NUMERIC.
856                            Some(s) => (SqliteType::Numeric, Some(s)),
857                        },
858                        primary_key,
859                        nullable,
860                        default_value,
861                        generated: def.constraints.iter().find_map(|named| {
862                            if let ColumnConstraint::Generated { ref expr, ref typ } =
863                                named.constraint
864                            {
865                                trace!("generated typ: {typ:?}, expr: {expr:?}");
866                                let mut from = vec![];
867                                extract_expr_columns(expr, &mut from);
868                                Some(Generated {
869                                    raw: expr.to_string(),
870                                    from,
871                                })
872                            } else {
873                                None
874                            }
875                        }),
876                        raw: def.clone(),
877                    },
878                )
879            })
880            .collect::<IndexMap<_, _>>(),
881        pk,
882        raw: CreateTableBody::ColumnsAndConstraints {
883            columns: columns
884                .iter()
885                .map(|col| (col.col_name.clone(), col.clone()))
886                .collect(),
887            constraints: constraints.cloned(),
888            flags: sqlite3_parser::ast::TabFlags::empty(),
889        },
890    }
891}
892
893fn extract_expr_columns(expr: &Expr, cols: &mut Vec<String>) {
894    match expr {
895        Expr::FunctionCall {
896            args: Some(args), ..
897        } => {
898            for expr in args.iter() {
899                extract_expr_columns(expr, cols);
900            }
901        }
902        Expr::Id(colname) => {
903            cols.push(unquote(&colname.0).ok().unwrap_or(colname.0.to_string()));
904        }
905        _ => {}
906    }
907}