Skip to main content

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(Box<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
210pub fn init_schema(conn: &Connection) -> Result<Schema, Box<SchemaError>> {
211    fn dump(conn: &Connection) -> Result<String, rusqlite::Error> {
212        let mut dump = String::new();
213
214        let tables: HashMap<String, String> = conn
215            .prepare(
216                r#"SELECT name, sql FROM __corro_schema WHERE type = "table" ORDER BY tbl_name"#,
217            )?
218            .query_map((), |row| {
219                Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
220            })?
221            .collect::<rusqlite::Result<_>>()?;
222
223        for sql in tables.values() {
224            dump.push_str(sql.as_str());
225            dump.push(';');
226        }
227
228        let indexes: HashMap<String, String> = conn
229            .prepare(
230                r#"SELECT name, sql FROM __corro_schema WHERE type = "index" ORDER BY tbl_name"#,
231            )?
232            .query_map((), |row| {
233                Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
234            })?
235            .collect::<rusqlite::Result<_>>()?;
236
237        for sql in indexes.values() {
238            dump.push_str(sql.as_str());
239            dump.push(';');
240        }
241
242        Ok(dump)
243    }
244
245    let dump = dump(conn).map_err(|err| Box::new(SchemaError::from(err)))?;
246    parse_sql(&dump)
247}
248
249#[derive(Debug)]
250pub struct ImportedSchemaPkMismatch {
251    pub tbl_name: String,
252    pub expected: IndexSet<String>,
253    pub got: IndexSet<String>,
254}
255
256#[derive(Debug)]
257pub struct ImportedSchemaColumnsMismatch {
258    pub tbl_name: String,
259    pub expected: IndexMap<String, Column>,
260    pub got: IndexMap<String, Column>,
261}
262
263#[derive(Debug, thiserror::Error)]
264pub enum ApplySchemaError {
265    #[error(transparent)]
266    Rusqlite(#[from] rusqlite::Error),
267    #[error(transparent)]
268    Schema(#[from] Box<SchemaError>),
269    #[error(transparent)]
270    ConstrainedSchema(#[from] Box<ConstrainedSchemaError>),
271    #[error("won't drop table without the destructive flag set (table: '{0}')")]
272    DropTableWithoutDestructiveFlag(String),
273    #[error("won't remove column without the destructive flag set (table: '{0}', column: '{1}')")]
274    RemoveColumnWithoutDestructiveFlag(String, String),
275    #[error("won't change column without the destructive flag set (table: '{0}', column: '{1}')")]
276    ChangeColumnWithoutDestructiveFlag(String, String),
277    #[error("can't add a primary key (table: '{0}', columns: '{1}')")]
278    AddPrimaryKey(String, String),
279    #[error("can't modify primary keys (table: '{0}')")]
280    ModifyPrimaryKeys(String),
281
282    #[error(
283        "tried importing an existing schema for table '{0}' due to a failed CREATE TABLE but didn't find anything (this should never happen)"
284    )]
285    ImportedSchemaNotFound(String),
286
287    #[error("existing schema for table '{}' primary keys mismatched, expected: {:?}, got: {:?}", .0.tbl_name, .0.expected, .0.got)]
288    ImportedSchemaPkMismatch(Box<ImportedSchemaPkMismatch>),
289
290    #[error("existing schema for table '{}' columns mismatched, expected: {:?}, got: {:?}", .0.tbl_name, .0.expected, .0.got)]
291    ImportedSchemaColumnsMismatch(Box<ImportedSchemaColumnsMismatch>),
292}
293
294pub fn apply_schema(
295    tx: &Transaction,
296    schema: &Schema,
297    new_schema: &mut Schema,
298) -> Result<(), ApplySchemaError> {
299    if let Some(name) = schema
300        .tables
301        .keys()
302        .collect::<HashSet<_>>()
303        .difference(&new_schema.tables.keys().collect::<HashSet<_>>())
304        .next()
305    {
306        // TODO: add options and check flag
307        return Err(ApplySchemaError::DropTableWithoutDestructiveFlag(
308            (*name).clone(),
309        ));
310    }
311
312    let mut schema_to_merge = Schema::default();
313
314    {
315        let new_table_names = new_schema
316            .tables
317            .keys()
318            .collect::<HashSet<_>>()
319            .difference(&schema.tables.keys().collect::<HashSet<_>>())
320            .cloned()
321            .collect::<HashSet<_>>();
322
323        debug!("new table names: {new_table_names:?}");
324
325        let new_tables_iter = new_schema
326            .tables
327            .iter()
328            .filter(|(table, _)| new_table_names.contains(table));
329
330        for (name, table) in new_tables_iter {
331            info!("creating table '{name}'");
332            let create_table_res = tx.execute_batch(
333                &Cmd::Stmt(Stmt::CreateTable {
334                    temporary: false,
335                    if_not_exists: false,
336                    tbl_name: QualifiedName::single(Name(name.to_string().into())),
337                    body: table.raw.clone(),
338                })
339                .to_string(),
340            );
341
342            if let Err(e) = create_table_res {
343                debug!(
344                    "could not create table '{name}', trying to reconcile schema if table already exists"
345                );
346                let sql: Vec<String> = tx
347                .prepare(
348                    "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(
349                    [name],
350                    |row| row.get(0),
351                )?.collect::<rusqlite::Result<Vec<_>>>()?;
352
353                if sql.is_empty() {
354                    return Err(e.into());
355                }
356
357                let sql = sql.join(";");
358                info!("found existing schema for '{name}'");
359
360                let parsed_table = parse_sql(&sql)?
361                    .tables
362                    .swap_remove(name)
363                    .ok_or_else(|| ApplySchemaError::ImportedSchemaNotFound(name.clone()))?;
364
365                if parsed_table.pk != table.pk {
366                    return Err(ApplySchemaError::ImportedSchemaPkMismatch(Box::new(
367                        ImportedSchemaPkMismatch {
368                            tbl_name: name.clone(),
369                            expected: table.pk.clone(),
370                            got: parsed_table.pk,
371                        },
372                    )));
373                }
374
375                if parsed_table.columns != table.columns {
376                    return Err(ApplySchemaError::ImportedSchemaColumnsMismatch(Box::new(
377                        ImportedSchemaColumnsMismatch {
378                            tbl_name: name.clone(),
379                            expected: table.columns.clone(),
380                            got: parsed_table.columns,
381                        },
382                    )));
383                }
384
385                schema_to_merge.tables.insert(name.clone(), parsed_table);
386            }
387
388            debug!("selecting crsql_as_crr");
389            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);"))?;
390            debug!("done selecting as crr");
391
392            if schema_to_merge.tables.contains_key(name) {
393                // just merged!
394                continue;
395            }
396
397            for (idx_name, index) in table.indexes.iter() {
398                info!("creating index '{idx_name}'");
399                tx.execute_batch(
400                    &Cmd::Stmt(Stmt::CreateIndex {
401                        unique: false,
402                        if_not_exists: false,
403                        idx_name: QualifiedName::single(Name(idx_name.clone().into())),
404                        tbl_name: Name(index.tbl_name.clone().into()),
405                        columns: index.columns.clone(),
406                        where_clause: index.where_clause.clone(),
407                    })
408                    .to_string(),
409                )?;
410            }
411        }
412    }
413
414    for (name, table) in schema_to_merge.tables {
415        new_schema.tables.insert(name, table);
416    }
417
418    // iterate intersecting tables
419    for name in new_schema
420        .tables
421        .keys()
422        .collect::<HashSet<_>>()
423        .intersection(&schema.tables.keys().collect::<HashSet<_>>())
424        .cloned()
425    {
426        debug!("processing table '{name}'");
427        let table = schema.tables.get(name).unwrap();
428        debug!(
429            "current cols: {:?}",
430            table.columns.keys().collect::<Vec<&String>>()
431        );
432        let new_table = new_schema.tables.get(name).unwrap();
433        debug!(
434            "new cols: {:?}",
435            new_table.columns.keys().collect::<Vec<&String>>()
436        );
437
438        // 1. Check column drops... don't allow unless flag is passed
439
440        let dropped_cols = table
441            .columns
442            .keys()
443            .collect::<HashSet<_>>()
444            .difference(&new_table.columns.keys().collect::<HashSet<_>>())
445            .cloned()
446            .collect::<HashSet<_>>();
447
448        debug!("dropped cols: {dropped_cols:?}");
449
450        if let Some(col_name) = dropped_cols.into_iter().next() {
451            return Err(ApplySchemaError::RemoveColumnWithoutDestructiveFlag(
452                name.clone(),
453                col_name.clone(),
454            ));
455        }
456
457        // 2. check for changed columns
458
459        let changed_cols: HashMap<String, Column> = table
460            .columns
461            .iter()
462            .filter_map(|(name, col)| {
463                new_table
464                    .columns
465                    .get(name)
466                    .and_then(|new_col| (new_col != col).then(|| (name.clone(), col.clone())))
467            })
468            .collect();
469
470        debug!(
471            "changed cols: {:?}",
472            changed_cols.keys().collect::<Vec<_>>()
473        );
474
475        if !changed_cols.is_empty() {
476            // TODO: add destructive flag
477            return Err(ApplySchemaError::ChangeColumnWithoutDestructiveFlag(
478                table.name.clone(),
479                changed_cols.keys().cloned().collect::<Vec<_>>().join(","),
480            ));
481        }
482
483        let new_col_names = new_table
484            .columns
485            .keys()
486            .collect::<HashSet<_>>()
487            .difference(&table.columns.keys().collect::<HashSet<_>>())
488            .cloned()
489            .collect::<HashSet<_>>();
490
491        info!("new columns: {new_col_names:?}");
492
493        if changed_cols.is_empty() {
494            // 2.1. no changed columns, add missing ones
495
496            if new_col_names.is_empty() {
497                // nothing to do
498            } else {
499                info!("Altering crsql for table {}", table.name);
500                let start = Instant::now();
501                let new_cols = new_table
502                    .columns
503                    .iter()
504                    .filter(|(col_name, _)| new_col_names.contains(col_name))
505                    .collect::<Vec<_>>();
506
507                // if all columns are generated, we don't need a migration
508                let require_migration = !new_cols.iter().all(|(_, col)| col.generated.is_some());
509
510                if require_migration {
511                    tx.execute_batch(&format!("SELECT crsql_begin_alter('{name}');"))?;
512                }
513
514                for (col_name, col) in new_cols {
515                    info!("adding column '{col_name}'");
516                    if col.primary_key {
517                        return Err(ApplySchemaError::AddPrimaryKey(
518                            name.clone(),
519                            col_name.clone(),
520                        ));
521                    }
522                    if !col.nullable && col.default_value.is_none() {
523                        return Err(Box::new(
524                            ConstrainedSchemaError::NotNullableColumnNeedsDefault {
525                                tbl_name: name.clone(),
526                                name: col_name.clone(),
527                            },
528                        )
529                        .into());
530                    }
531                    tx.execute_batch(&format!("ALTER TABLE {name} ADD COLUMN {col}"))?;
532                }
533
534                if require_migration {
535                    tx.execute_batch(&format!("SELECT crsql_commit_alter('main', '{name}', 1);"))?;
536                }
537                info!(
538                    "Altering crsql for table {} took {:?}",
539                    table.name,
540                    start.elapsed()
541                );
542            }
543        } else {
544            // 2.2 we do have changed columns, try to do something about that
545
546            info!("Columns have changed... replacing table {}", table.name);
547            let start = Instant::now();
548
549            let primary_keys = table
550                .columns
551                .values()
552                .filter_map(|col| col.primary_key.then_some(&col.name))
553                .collect::<Vec<&String>>();
554
555            let new_primary_keys = new_table
556                .columns
557                .values()
558                .filter_map(|col| col.primary_key.then_some(&col.name))
559                .collect::<Vec<&String>>();
560
561            if primary_keys != new_primary_keys {
562                return Err(ApplySchemaError::ModifyPrimaryKeys(name.clone()));
563            }
564
565            // "12-step" process to modifying a table
566
567            // first, create our new table with a temp name
568            let tmp_name = format!(
569                "{name}_{}",
570                SystemTime::now()
571                    .duration_since(SystemTime::UNIX_EPOCH)
572                    .unwrap()
573                    .as_millis()
574            );
575
576            let create_tmp_table = Cmd::Stmt(Stmt::CreateTable {
577                temporary: false,
578                if_not_exists: false,
579                tbl_name: QualifiedName::single(Name(tmp_name.clone().into())),
580                body: new_table.raw.clone(),
581            });
582
583            tx.execute_batch("SELECT crsql_begin_alter('{name}');")?;
584
585            info!("creating tmp table '{tmp_name}'");
586            tx.execute_batch(&create_tmp_table.to_string())?;
587
588            let col_names = table
589                .columns
590                .keys()
591                .cloned()
592                .collect::<Vec<String>>()
593                .join(",");
594
595            info!("inserting data from '{name}' into '{tmp_name}'");
596            let inserted = tx.execute(
597                &format!("INSERT INTO {tmp_name} ({col_names}) SELECT {col_names} FROM {name}"),
598                (),
599            )?;
600
601            info!("re-inserted {inserted} rows into the new table for {name}");
602
603            info!("dropping old table '{name}', renaming '{tmp_name}' to '{name}'");
604            tx.execute_batch(&format!(
605                "DROP TABLE {name};
606                 ALTER TABLE {tmp_name} RENAME TO {name}"
607            ))?;
608
609            tx.execute_batch(&format!("SELECT crsql_commit_alter('{name}');"))?;
610            info!("Replacing table {} took {:?}", table.name, start.elapsed());
611        }
612
613        let new_index_names = new_table
614            .indexes
615            .keys()
616            .collect::<HashSet<_>>()
617            .difference(&table.indexes.keys().collect::<HashSet<_>>())
618            .cloned()
619            .collect::<HashSet<_>>();
620
621        let new_indexes_iter = new_table
622            .indexes
623            .iter()
624            .filter(|(index, _)| new_index_names.contains(index));
625
626        for (idx_name, index) in new_indexes_iter {
627            info!("creating new index '{idx_name}'");
628            tx.execute_batch(
629                &Cmd::Stmt(Stmt::CreateIndex {
630                    unique: false,
631                    if_not_exists: false,
632                    idx_name: QualifiedName::single(Name(idx_name.clone().into())),
633                    tbl_name: Name(index.tbl_name.clone().into()),
634                    columns: index.columns.clone(),
635                    where_clause: index.where_clause.clone(),
636                })
637                .to_string(),
638            )?;
639        }
640
641        let dropped_indexes = table
642            .indexes
643            .keys()
644            .collect::<HashSet<_>>()
645            .difference(&new_table.indexes.keys().collect::<HashSet<_>>())
646            .cloned()
647            .collect::<HashSet<_>>();
648
649        for idx_name in dropped_indexes {
650            info!("dropping index '{idx_name}'");
651            tx.execute_batch(&format!("DROP INDEX {idx_name}"))?;
652        }
653
654        let changed_indexes_iter = table.indexes.iter().filter_map(|(idx_name, index)| {
655            let pindex = new_table.indexes.get(idx_name)?;
656            if pindex != index {
657                Some((idx_name, pindex))
658            } else {
659                None
660            }
661        });
662
663        for (idx_name, index) in changed_indexes_iter {
664            info!("replacing index '{idx_name}' (drop + create)");
665            tx.execute_batch(&format!(
666                "DROP INDEX {idx_name}; {}",
667                &Cmd::Stmt(Stmt::CreateIndex {
668                    unique: false,
669                    if_not_exists: false,
670                    idx_name: QualifiedName::single(Name(idx_name.clone().into())),
671                    tbl_name: Name(index.tbl_name.clone().into()),
672                    columns: index.columns.clone(),
673                    where_clause: index.where_clause.clone(),
674                })
675                .to_string(),
676            ))?;
677        }
678    }
679
680    Ok(())
681}
682
683pub fn parse_sql_to_schema(schema: &mut Schema, sql: &str) -> Result<(), Box<SchemaError>> {
684    trace!("parsing {sql}");
685    let mut parser = sqlite3_parser::lexer::sql::Parser::new(sql.as_bytes());
686
687    loop {
688        match parser.next() {
689            Ok(None) => break,
690            Err(err) => {
691                eprintln!("Err: {err}");
692                return Err(Box::new(err.into()));
693            }
694            Ok(Some(ref cmd @ Cmd::Stmt(ref stmt))) => match stmt {
695                Stmt::CreateTable {
696                    temporary: true, ..
697                } => return Err(Box::new(SchemaError::TemporaryTable(cmd.clone()))),
698                Stmt::CreateTable {
699                    body: CreateTableBody::AsSelect(_),
700                    ..
701                } => return Err(Box::new(SchemaError::TemporaryTable(cmd.clone()))),
702                Stmt::CreateTable {
703                    temporary: false,
704                    if_not_exists: _,
705                    tbl_name,
706                    body:
707                        CreateTableBody::ColumnsAndConstraints {
708                            columns,
709                            constraints,
710                            ..
711                        },
712                } => {
713                    let table = prepare_table(
714                        tbl_name,
715                        &columns.values().cloned().collect::<Vec<_>>(),
716                        constraints.as_ref(),
717                    );
718                    schema.tables.insert(table.name.clone(), table);
719                    trace!("inserted table: {}", tbl_name.name.0);
720                }
721                Stmt::CreateIndex {
722                    unique,
723                    idx_name,
724                    tbl_name,
725                    columns,
726                    where_clause,
727                    ..
728                } => {
729                    let tbl_name = unquote(&tbl_name.0).unwrap_or_else(|_| tbl_name.0.to_string());
730                    let idx_name =
731                        unquote(&idx_name.name.0).unwrap_or_else(|_| idx_name.name.0.to_string());
732                    if let Some(table) = schema.tables.get_mut(tbl_name.as_str()) {
733                        table.indexes.insert(
734                            idx_name.clone(),
735                            Index {
736                                name: idx_name,
737                                tbl_name,
738                                columns: columns.to_vec(),
739                                where_clause: where_clause.clone(),
740                                unique: *unique,
741                            },
742                        );
743                    } else {
744                        return Err(Box::new(SchemaError::IndexWithoutTable {
745                            tbl_name: tbl_name.clone(),
746                            name: idx_name.clone(),
747                        }));
748                    }
749                }
750                _ => return Err(Box::new(SchemaError::UnsupportedCmd(cmd.clone()))),
751            },
752            Ok(Some(cmd)) => return Err(Box::new(SchemaError::UnsupportedCmd(cmd))),
753        }
754    }
755
756    Ok(())
757}
758
759pub fn parse_sql(sql: &str) -> Result<Schema, Box<SchemaError>> {
760    let mut schema = Schema::default();
761
762    parse_sql_to_schema(&mut schema, sql)?;
763
764    Ok(schema)
765}
766
767#[allow(clippy::result_large_err)]
768fn prepare_table(
769    tbl_name: &QualifiedName,
770    columns: &[ColumnDefinition],
771    constraints: Option<&Vec<NamedTableConstraint>>,
772) -> Table {
773    let pk = constraints
774        .and_then(|constraints| {
775            constraints
776                .iter()
777                .find_map(|named| match &named.constraint {
778                    TableConstraint::PrimaryKey { columns, .. } => Some(
779                        columns
780                            .iter()
781                            .filter_map(|col| match &col.expr {
782                                Expr::Id(id) => {
783                                    Some(unquote(&id.0).unwrap_or_else(|_| id.0.to_string()))
784                                }
785                                _ => None,
786                            })
787                            .collect::<IndexSet<_>>(),
788                    ),
789                    _ => None,
790                })
791        })
792        .unwrap_or_else(|| {
793            columns
794                .iter()
795                .filter(|&def| {
796                    def.constraints.iter().any(|named| {
797                        matches!(named.constraint, ColumnConstraint::PrimaryKey { .. })
798                    })
799                })
800                .map(|def| unquote(&def.col_name.0).unwrap_or_else(|_| def.col_name.0.to_string()))
801                .collect()
802        });
803
804    Table {
805        name: unquote(&tbl_name.name.0).unwrap_or_else(|_| tbl_name.name.0.to_string()),
806        indexes: IndexMap::new(),
807        columns: columns
808            .iter()
809            .map(|def| {
810                trace!("visiting column: {}", def.col_name.0);
811                let default_value = def.constraints.iter().find_map(|named| {
812                    if let ColumnConstraint::Default(ref expr) = named.constraint {
813                        Some(expr.to_string())
814                    } else {
815                        None
816                    }
817                });
818
819                let not_nullable = def.constraints.iter().any(|named| {
820                    matches!(
821                        named.constraint,
822                        ColumnConstraint::NotNull {
823                            nullable: false,
824                            ..
825                        }
826                    )
827                });
828                let nullable = !not_nullable;
829
830                let primary_key = pk.contains(def.col_name.0.as_ref());
831
832                let col_name =
833                    unquote(&def.col_name.0).unwrap_or_else(|_| def.col_name.0.to_string());
834
835                (
836                    col_name.clone(),
837                    Column {
838                        name: col_name.clone(),
839                        sql_type: match def.col_type.as_ref().map(|t| t.name.to_ascii_uppercase()) {
840                            // 1. If the declared type contains the string "INT" then it is assigned INTEGER affinity.
841                            Some(s) if s.contains("INT") => (SqliteType::Integer, Some(s)),
842                            // 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.
843                            Some(s)
844                                if s.contains("CHAR")
845                                    || s.contains("CLOB")
846                                    || s.contains("TEXT")
847                                    || s == "JSON" =>
848                            {
849                                (SqliteType::Text, Some(s))
850                            }
851
852                            // 3. If the declared type for a column contains the string "BLOB" or if no type is specified then the column has affinity BLOB.
853                            Some(s) if s.contains("BLOB") || s == "JSONB" => {
854                                (SqliteType::Blob, Some(s))
855                            }
856                            None => (SqliteType::Blob, None),
857
858                            // 4. If the declared type for a column contains any of the strings "REAL", "FLOA", or "DOUB" then the column has REAL affinity.
859                            Some(s)
860                                if s.contains("REAL")
861                                    || s.contains("FLOA")
862                                    || s.contains("DOUB")
863                                    || s == "ANY" =>
864                            {
865                                (SqliteType::Real, Some(s))
866                            }
867
868                            // 5. Otherwise, the affinity is NUMERIC.
869                            Some(s) => (SqliteType::Numeric, Some(s)),
870                        },
871                        primary_key,
872                        nullable,
873                        default_value,
874                        generated: def.constraints.iter().find_map(|named| {
875                            if let ColumnConstraint::Generated { ref expr, ref typ } =
876                                named.constraint
877                            {
878                                trace!("generated typ: {typ:?}, expr: {expr:?}");
879                                let mut from = vec![];
880                                extract_expr_columns(expr, &mut from);
881                                Some(Generated {
882                                    raw: expr.to_string(),
883                                    from,
884                                })
885                            } else {
886                                None
887                            }
888                        }),
889                        raw: def.clone(),
890                    },
891                )
892            })
893            .collect::<IndexMap<_, _>>(),
894        pk,
895        raw: CreateTableBody::ColumnsAndConstraints {
896            columns: columns
897                .iter()
898                .map(|col| (col.col_name.clone(), col.clone()))
899                .collect(),
900            constraints: constraints.cloned(),
901            flags: sqlite3_parser::ast::TabFlags::empty(),
902        },
903    }
904}
905
906fn extract_expr_columns(expr: &Expr, cols: &mut Vec<String>) {
907    match expr {
908        Expr::FunctionCall {
909            args: Some(args), ..
910        } => {
911            for expr in args.iter() {
912                extract_expr_columns(expr, cols);
913            }
914        }
915        Expr::Id(colname) => {
916            cols.push(unquote(&colname.0).ok().unwrap_or(colname.0.to_string()));
917        }
918        _ => {}
919    }
920}