Skip to main content

trailbase_schema/
sqlite.rs

1use fallible_iterator::FallibleIterator;
2use itertools::Itertools;
3use log::*;
4use serde::{Deserialize, Serialize};
5use sqlite3_parser::ast::{
6  ColumnDefinition, CreateTableBody, DeferSubclause, Expr, ForeignKeyClause, FromClause,
7  IndexedColumn, Literal, Name, QualifiedName as AstQualifiedName, SelectTable, Stmt, TabFlags,
8  TableConstraint, fmt::ToTokens,
9};
10use std::collections::HashMap;
11use std::hash::{Hash, Hasher};
12use thiserror::Error;
13use ts_rs::TS;
14
15#[derive(Debug, Error)]
16pub enum SchemaError {
17  #[error("Missing ObjectName")]
18  MissingName,
19  #[error("Precondition failed: {0}")]
20  Precondition(Box<dyn std::error::Error + Send + Sync>),
21}
22
23pub fn sqlite3_parse_into_statements(
24  sql: &str,
25) -> Result<Vec<Stmt>, sqlite3_parser::lexer::sql::Error> {
26  use sqlite3_parser::ast::Cmd;
27
28  // According to sqlite3_parser's docs they're working to remove panics in some edge cases.
29  // Meanwhile we'll trap them here. We haven't seen any in practice yet.
30  let outer_result = std::panic::catch_unwind(|| {
31    let mut parser = sqlite3_parser::lexer::sql::Parser::new(sql.as_bytes());
32
33    let mut statements: Vec<Stmt> = vec![];
34    while let Some(cmd) = parser.next()? {
35      match cmd {
36        Cmd::Stmt(stmt) => {
37          statements.push(stmt);
38        }
39        Cmd::Explain(_) | Cmd::ExplainQueryPlan(_) => {}
40      }
41    }
42    return Ok(statements);
43  });
44
45  return match outer_result {
46    Ok(inner_result) => inner_result,
47    Err(_panic_err) => {
48      error!("Parser panicked");
49      return Err(sqlite3_parser::lexer::sql::Error::UnrecognizedToken(None));
50    }
51  };
52}
53
54pub fn sqlite3_parse_into_statement(
55  sql: &str,
56) -> Result<Option<Stmt>, sqlite3_parser::lexer::sql::Error> {
57  use sqlite3_parser::ast::Cmd;
58
59  // According to sqlite3_parser's docs they're working to remove panics in some edge cases.
60  // Meanwhile we'll trap them here. We haven't seen any in practice yet.
61  let outer_result = std::panic::catch_unwind(|| {
62    let mut parser = sqlite3_parser::lexer::sql::Parser::new(sql.as_bytes());
63
64    while let Some(cmd) = parser.next()? {
65      match cmd {
66        Cmd::Stmt(stmt) => {
67          return Ok(Some(stmt));
68        }
69        Cmd::Explain(_) | Cmd::ExplainQueryPlan(_) => {}
70      }
71    }
72    return Ok(None);
73  });
74
75  return match outer_result {
76    Ok(inner_result) => inner_result,
77    Err(_panic_err) => {
78      error!("Parser panicked");
79      return Err(sqlite3_parser::lexer::sql::Error::UnrecognizedToken(None));
80    }
81  };
82}
83
84// This file contains table schema and index representations. Originally, they were mostly
85// adaptations of sqlparser's CreateX AST representations (we've since moved to sqlite3_parser).
86// This serves two purposes:
87//
88//  * We'd like some representation that we can construct on the client with type-safety. We could
89//    also consider using proto here, but ts-rs let's us "skip" some fields.
90//  * But also, there's a fundamental difference between an AST that represents a specific SQL
91//    program and a more abstract semantic representation of the schema, e.g. we don't care in which
92//    order indexes were constructed or what quotes were used...
93//
94// NOTE: We're very much "over-wrapping" here entering the space of the exact-program AST domain.
95// This is mostly convenient for testing our code by transforming back and forth and checking the
96// output is stable. We can use "skip" to remove some more "representational" details from the API.
97#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
98pub struct ForeignKey {
99  pub name: Option<String>,
100  pub columns: Vec<String>,
101  pub foreign_table: String,
102  pub referred_columns: Vec<String>,
103
104  // Only "ON DELETE" and "ON UPDATE" are supported in foreign key clause, i.e. no "ON INSERT":
105  //   https://www.sqlite.org/syntax/foreign-key-clause.html
106  pub on_delete: Option<ReferentialAction>,
107  pub on_update: Option<ReferentialAction>,
108  // TODO: Missing DEFERRABLE.
109}
110
111impl ForeignKey {
112  fn to_fragment(&self) -> String {
113    let cols = quote(&self.columns);
114    let foreign_table = &self.foreign_table;
115    let ref_col = match self.referred_columns.len() {
116      0 => "".to_string(),
117      _ => format!("({})", quote(&self.referred_columns)),
118    };
119
120    let on_delete = self.on_delete.as_ref().map_or_else(
121      || "".to_string(),
122      |action| format!("ON DELETE {}", action.to_fragment()),
123    );
124    let on_update = self.on_update.as_ref().map_or_else(
125      || "".to_string(),
126      |action| format!("ON UPDATE {}", action.to_fragment()),
127    );
128
129    return if let Some(ref name) = self.name {
130      format!(
131        "CONSTRAINT '{name}' FOREIGN KEY ({cols}) REFERENCES '{foreign_table}'{ref_col} {on_delete} {on_update}"
132      )
133    } else {
134      format!("FOREIGN KEY ({cols}) REFERENCES '{foreign_table}'{ref_col} {on_delete} {on_update}")
135    };
136  }
137}
138
139#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
140pub struct Check {
141  pub name: Option<String>,
142  pub expr: String,
143}
144
145impl Check {
146  fn to_fragment(&self) -> String {
147    return if let Some(ref name) = self.name {
148      format!("CONSTRAINT '{name}' CHECK({})", self.expr)
149    } else {
150      format!("CHECK({})", self.expr)
151    };
152  }
153}
154
155// https://www.sqlite.org/syntax/table-constraint.html.
156#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
157pub struct UniqueConstraint {
158  pub name: Option<String>,
159
160  /// Identifiers of the columns that are unique.
161  ///
162  /// TODO: Should be indexed/ordered column, e.g. ASC/DESC:
163  ///   https://www.sqlite.org/syntax/indexed-column.html
164  pub columns: Vec<String>,
165
166  pub conflict_clause: Option<ConflictResolution>,
167}
168
169impl UniqueConstraint {
170  fn to_fragment(&self) -> String {
171    let cols = quote(&self.columns);
172
173    return match (self.name.as_ref(), &self.conflict_clause.as_ref()) {
174      (Some(name), Some(resolution)) => format!(
175        "CONSTRAINT '{name}' UNIQUE ({cols}) ON CONFLICT {}",
176        resolution.to_fragment()
177      ),
178      (Some(name), None) => format!("CONSTRAINT '{name}' UNIQUE ({cols})"),
179      (None, Some(resolution)) => {
180        format!("UNIQUE ({cols}) ON CONFLICT {}", resolution.to_fragment())
181      }
182      (None, None) => format!("UNIQUE ({cols})"),
183    };
184  }
185}
186
187#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
188pub struct ColumnOrder {
189  pub column_name: String,
190  pub ascending: Option<bool>,
191  pub nulls_first: Option<bool>,
192}
193
194/// Conflict resolution types
195#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
196pub enum ConflictResolution {
197  /// `ROLLBACK`
198  Rollback,
199  /// `ABORT`
200  Abort, // default
201  /// `FAIL`
202  Fail,
203  /// `IGNORE`
204  Ignore,
205  /// `REPLACE`
206  Replace,
207}
208
209impl From<sqlite3_parser::ast::ResolveType> for ConflictResolution {
210  fn from(res: sqlite3_parser::ast::ResolveType) -> Self {
211    use sqlite3_parser::ast::ResolveType;
212    match res {
213      ResolveType::Rollback => ConflictResolution::Rollback,
214      ResolveType::Abort => ConflictResolution::Abort,
215      ResolveType::Fail => ConflictResolution::Fail,
216      ResolveType::Ignore => ConflictResolution::Ignore,
217      ResolveType::Replace => ConflictResolution::Replace,
218    }
219  }
220}
221
222impl ConflictResolution {
223  // https://www.sqlite.org/syntax/conflict-clause.html
224  fn to_fragment(&self) -> &'static str {
225    return match self {
226      Self::Rollback => "ROLLBACK",
227      Self::Abort => "ABORT",
228      Self::Fail => "FAIL",
229      Self::Ignore => "IGNORE",
230      Self::Replace => "REPLACE",
231    };
232  }
233}
234
235#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
236pub enum ReferentialAction {
237  Restrict,
238  Cascade,
239  SetNull,
240  NoAction,
241  SetDefault,
242}
243
244impl From<sqlite3_parser::ast::RefAct> for ReferentialAction {
245  fn from(action: sqlite3_parser::ast::RefAct) -> Self {
246    use sqlite3_parser::ast::RefAct;
247    match action {
248      RefAct::Restrict => ReferentialAction::Restrict,
249      RefAct::Cascade => ReferentialAction::Cascade,
250      RefAct::SetNull => ReferentialAction::SetNull,
251      RefAct::NoAction => ReferentialAction::NoAction,
252      RefAct::SetDefault => ReferentialAction::SetDefault,
253    }
254  }
255}
256
257impl ReferentialAction {
258  // https://www.sqlite.org/syntax/foreign-key-clause.html
259  fn to_fragment(&self) -> &'static str {
260    return match self {
261      Self::Restrict => "RESTRICT",
262      Self::Cascade => "CASCADE",
263      Self::SetNull => "SET NULL",
264      Self::NoAction => "NO ACTION",
265      Self::SetDefault => "SET DEFAULT",
266    };
267  }
268}
269
270#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
271pub enum GeneratedExpressionMode {
272  Virtual,
273  Stored,
274}
275
276#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
277pub enum ColumnOption {
278  Null,
279  NotNull,
280  Default(String),
281  // NOTE: Unique { is_primary: true} means PrimaryKey.
282  Unique {
283    is_primary: bool,
284    conflict_clause: Option<ConflictResolution>,
285    // TODO: Missing ASC/DESC & AUTOINCREMENT for PK.
286  },
287  ForeignKey {
288    foreign_table: String,
289    referred_columns: Vec<String>,
290    on_delete: Option<ReferentialAction>,
291    on_update: Option<ReferentialAction>,
292  },
293  Check(String),
294  OnUpdate(String),
295  Generated {
296    expr: String,
297    mode: Option<GeneratedExpressionMode>,
298  },
299}
300
301impl ColumnOption {
302  fn to_fragment(&self) -> String {
303    return match self {
304      Self::Null => "NULL".to_string(),
305      Self::NotNull => "NOT NULL".to_string(),
306      Self::Default(v) => format!("DEFAULT {v}"),
307      Self::Unique {
308        is_primary,
309        conflict_clause,
310      } => match (*is_primary, conflict_clause.as_ref()) {
311        (true, Some(res)) => format!("PRIMARY KEY ON CONFLICT {}", res.to_fragment()),
312        (true, None) => "PRIMARY KEY".to_string(),
313        (false, Some(res)) => format!("UNIQUE ON CONFLICT {}", res.to_fragment()),
314        (false, None) => "UNIQUE".to_string(),
315      },
316      Self::ForeignKey {
317        foreign_table,
318        referred_columns,
319        on_delete,
320        on_update,
321      } => {
322        format!(
323          "REFERENCES '{foreign_table}'{ref_col} {on_delete} {on_update}",
324          ref_col = match referred_columns.len() {
325            0 => "".to_string(),
326            _ => format!("({})", quote(referred_columns)),
327          },
328          on_delete = on_delete.as_ref().map_or_else(
329            || "".to_string(),
330            |action| format!("ON DELETE {}", action.to_fragment())
331          ),
332          on_update = on_update.as_ref().map_or_else(
333            || "".to_string(),
334            |action| format!("ON UPDATE {}", action.to_fragment())
335          ),
336        )
337      }
338      Self::Check(expr) => format!("CHECK({expr})"),
339      Self::OnUpdate(expr) => expr.clone(),
340      Self::Generated { expr, mode } => format!(
341        "GENERATED ALWAYS AS ({expr}) {m}",
342        m = match mode {
343          Some(GeneratedExpressionMode::Stored) => "STORED",
344          Some(GeneratedExpressionMode::Virtual) => "VIRTUAL",
345          None => "",
346        }
347      ),
348    };
349  }
350}
351
352#[derive(Clone, Copy, Debug, Serialize, Deserialize, TS, PartialEq)]
353pub enum ColumnDataType {
354  Null,
355
356  // Strict column/storage types.
357  Any,
358  Blob,
359  Text,
360  Integer,
361  Real,
362  Numeric, // not allowed in strict mode.
363
364  // Other higher-level or affine types.
365  #[allow(clippy::upper_case_acronyms)]
366  JSON,
367  #[allow(clippy::upper_case_acronyms)]
368  JSONB,
369
370  // See 3.1.1. https://www.sqlite.org/datatype3.html.
371  //
372  // Types with INTEGER affinity.
373  Int,
374  TinyInt,
375  SmallInt,
376  MediumInt,
377  BigInt,
378  UnignedBigInt,
379  Int2,
380  Int4,
381  Int8,
382
383  // Types with TEXT affinity.
384  Character,
385  Varchar,
386  VaryingCharacter,
387  NChar,
388  NativeCharacter,
389  NVarChar,
390  Clob,
391
392  // Types with REAL affinity.
393  Double,
394  DoublePrecision,
395  Float,
396
397  // Types with NUMERIC affinity.
398  Boolean,
399  Decimal,
400  Date,
401  DateTime,
402}
403
404impl ColumnDataType {
405  fn from_type_name(type_name: &str) -> Option<Self> {
406    return Some(match type_name.to_uppercase().as_str() {
407      "UNSPECIFIED" => ColumnDataType::Null,
408      "ANY" => ColumnDataType::Any,
409      "BLOB" => ColumnDataType::Blob,
410      "TEXT" => ColumnDataType::Text,
411      "INTEGER" => ColumnDataType::Integer,
412      "REAL" => ColumnDataType::Real,
413      "NUMERIC" => ColumnDataType::Numeric,
414
415      // JSON types,
416      "JSON" => ColumnDataType::JSON,
417      "JSONB" => ColumnDataType::JSONB,
418
419      // See 3.1.1. https://www.sqlite.org/datatype3.html.
420      //
421      // Types with INTEGER affinity.
422      "INT" => ColumnDataType::Int,
423      "TINYINT" => ColumnDataType::TinyInt,
424      "SMALLINT" => ColumnDataType::SmallInt,
425      "MEDIUMINT" => ColumnDataType::MediumInt,
426      "BIGINT" => ColumnDataType::BigInt,
427      "UNSIGNED BIG INT" => ColumnDataType::UnignedBigInt,
428      "INT2" => ColumnDataType::Int2,
429      "INT4" => ColumnDataType::Int4,
430      "INT8" => ColumnDataType::Int8,
431
432      // Types with TEXT affinity.
433      "CHARACTER" => ColumnDataType::Character,
434      "VARCHAR" => ColumnDataType::Varchar,
435      "VARYING CHARACTER" => ColumnDataType::VaryingCharacter,
436      "NCHAR" => ColumnDataType::NChar,
437      "NATIVE CHARACTER" => ColumnDataType::NativeCharacter,
438      "NVARCHAR" => ColumnDataType::NVarChar,
439      "CLOB" => ColumnDataType::Clob,
440
441      // Types with REAL affinity.
442      "DOUBLE" => ColumnDataType::Double,
443      "DOUBLE PRECISION" => ColumnDataType::DoublePrecision,
444      "FLOAT" => ColumnDataType::Float,
445
446      // Types with NUMERIC affinity.
447      "BOOLEAN" => ColumnDataType::Boolean,
448      "DECIMAL" => ColumnDataType::Decimal,
449      "DATE" => ColumnDataType::Date,
450      "DATETIME" => ColumnDataType::DateTime,
451
452      _x => {
453        debug!("Unexpected data type: {_x:?}");
454        return None;
455      }
456    });
457  }
458}
459
460#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
461pub struct Column {
462  pub name: String,
463  pub data_type: ColumnDataType,
464  pub options: Vec<ColumnOption>,
465}
466
467impl Column {
468  fn to_fragment(&self) -> String {
469    let options: Vec<String> = self.options.iter().map(|o| o.to_fragment()).collect();
470
471    return if options.is_empty() {
472      format!(
473        "'{name}' {data_type}",
474        name = self.name,
475        data_type = format!("{:?}", self.data_type).to_uppercase(),
476      )
477    } else {
478      format!(
479        "'{name}' {data_type} {options}",
480        name = self.name,
481        data_type = format!("{:?}", self.data_type).to_uppercase(),
482        options = options.join(" "),
483      )
484    };
485  }
486
487  pub fn is_not_null(&self) -> bool {
488    return self
489      .options
490      .iter()
491      .any(|opt| matches!(opt, ColumnOption::NotNull));
492  }
493
494  pub fn has_default(&self) -> bool {
495    return self
496      .options
497      .iter()
498      .any(|opt| matches!(opt, ColumnOption::Default(_)));
499  }
500
501  pub fn is_primary(&self) -> bool {
502    return self.options.iter().any(
503      |opt| matches!(opt, ColumnOption::Unique { is_primary, conflict_clause: _ } if *is_primary ),
504    );
505  }
506}
507
508#[derive(Clone, Default, Debug, Serialize, Deserialize, TS)]
509pub struct QualifiedName {
510  pub name: String,
511  pub database_schema: Option<String>,
512}
513
514impl QualifiedName {
515  pub fn parse(name: &str) -> Result<Self, SchemaError> {
516    if name.contains(';') {
517      return Err(SchemaError::Precondition("Invalid name".into()));
518    }
519
520    if let Some((db, name)) = name.split_once('.') {
521      return Ok(Self {
522        name: unquote_string(name),
523        database_schema: Some(unquote_string(db)),
524      });
525    }
526    return Ok(Self {
527      name: unquote_string(name),
528      database_schema: None,
529    });
530  }
531
532  pub fn escaped_string(&self) -> String {
533    return if let Some(ref db) = self.database_schema {
534      format!(r#""{db}"."{}""#, self.name)
535    } else {
536      format!(r#""{}""#, self.name)
537    };
538  }
539
540  pub fn migration_filename(&self, prefix: &str) -> String {
541    return if let Some(ref db) = self.database_schema {
542      format!("{prefix}_{db}_{}", self.name)
543    } else {
544      format!("{prefix}_{}", self.name)
545    };
546  }
547}
548
549impl PartialEq for QualifiedName {
550  fn eq(&self, other: &Self) -> bool {
551    return self.name == other.name
552      && self.database_schema.as_deref().unwrap_or("main")
553        == other.database_schema.as_deref().unwrap_or("main");
554  }
555}
556
557impl Eq for QualifiedName {}
558
559impl Hash for QualifiedName {
560  fn hash<H: Hasher>(&self, state: &mut H) {
561    self.name.hash(state);
562    self
563      .database_schema
564      .as_deref()
565      .unwrap_or("main")
566      .hash(state);
567  }
568}
569
570impl From<AstQualifiedName> for QualifiedName {
571  fn from(qn: AstQualifiedName) -> Self {
572    return Self {
573      database_schema: unquote_db_name(&qn),
574      name: unquote_qualified(qn),
575    };
576  }
577}
578
579#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
580#[ts(export)]
581pub struct Table {
582  pub name: QualifiedName,
583  pub strict: bool,
584
585  // Column definition and column-level constraints.
586  pub columns: Vec<Column>,
587
588  // Table-level constraints, e.g. composite uniqueness or foreign keys. Columns may have their own
589  // column-level constraints a.k.a. Column::options.
590  pub foreign_keys: Vec<ForeignKey>,
591  pub unique: Vec<UniqueConstraint>,
592  pub checks: Vec<Check>,
593
594  // NOTE: consider parsing "CREATE VIRTUAL TABLE" into a separate struct.
595  pub virtual_table: bool,
596  pub temporary: bool,
597}
598
599impl Table {
600  pub fn create_table_statement(&self) -> String {
601    if self.virtual_table {
602      // https://www.sqlite.org/lang_createvtab.html
603      panic!("Not implemented");
604    }
605
606    let mut column_defs_and_table_constraints: Vec<String> = vec![];
607
608    column_defs_and_table_constraints.extend(self.columns.iter().map(|c| c.to_fragment()));
609
610    // Example: UNIQUE (email),
611    column_defs_and_table_constraints.extend(self.unique.iter().map(|unique| unique.to_fragment()));
612
613    // Example: FOREIGN KEY(user_id) REFERENCES table(id) ON DELETE CASCADE
614    column_defs_and_table_constraints.extend(self.foreign_keys.iter().map(|fk| fk.to_fragment()));
615
616    // Example: CHECK('age' > 0)
617    column_defs_and_table_constraints.extend(self.checks.iter().map(|fk| fk.to_fragment()));
618
619    return format!(
620      "CREATE{temporary} TABLE {fq_name} ({col_defs_and_constraints}){strict}",
621      temporary = if self.temporary { " TEMPORARY" } else { "" },
622      fq_name = self.name.escaped_string(),
623      col_defs_and_constraints = column_defs_and_table_constraints.join(", "),
624      strict = if self.strict { " STRICT" } else { "" },
625    );
626  }
627}
628
629#[derive(Clone, Default, Debug, Serialize, Deserialize, TS, PartialEq)]
630pub struct TableIndex {
631  pub name: QualifiedName,
632
633  pub table_name: String,
634  pub columns: Vec<ColumnOrder>,
635  pub unique: bool,
636  pub predicate: Option<String>,
637
638  #[ts(skip)]
639  #[serde(default)]
640  pub if_not_exists: bool,
641}
642
643impl TableIndex {
644  pub fn create_index_statement(&self) -> String {
645    let indexed_columns = self
646      .columns
647      .iter()
648      .map(|c| {
649        format!(
650          "'{name}' {order}",
651          name = c.column_name,
652          order = c
653            .ascending
654            .map_or("", |asc| if asc { "ASC" } else { "DESC" })
655        )
656      })
657      .join(", ");
658
659    return format!(
660      "CREATE{unique} INDEX {if_not_exists} {fqn_name} ON '{table_name}' ({indexed_columns}) {predicate}",
661      unique = if self.unique { " UNIQUE" } else { "" },
662      if_not_exists = if self.if_not_exists {
663        "IF NOT EXISTS"
664      } else {
665        ""
666      },
667      fqn_name = self.name.escaped_string(),
668      table_name = self.table_name,
669      predicate = self
670        .predicate
671        .as_ref()
672        .map_or_else(|| "".to_string(), |p| format!("WHERE {p}")),
673    );
674  }
675}
676
677#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
678pub struct View {
679  pub name: QualifiedName,
680
681  /// Columns may be inferred from a view's query.
682  ///
683  /// Views can be defined with arbitrary queries referencing arbitrary sources: tables, views,
684  /// functions, ..., which makes them inherently not type safe and therefore their columns not
685  /// well defined.
686  pub columns: Option<Vec<Column>>,
687
688  pub query: String,
689
690  pub temporary: bool,
691
692  #[ts(skip)]
693  pub if_not_exists: bool,
694}
695
696impl TryFrom<sqlite3_parser::ast::Stmt> for Table {
697  type Error = SchemaError;
698
699  fn try_from(value: sqlite3_parser::ast::Stmt) -> Result<Self, Self::Error> {
700    return match value {
701      Stmt::CreateTable {
702        temporary,
703        tbl_name,
704        body,
705        ..
706      } => {
707        let CreateTableBody::ColumnsAndConstraints {
708          columns,
709          constraints,
710          flags,
711        } = body
712        else {
713          return Err(SchemaError::Precondition(
714            "expected cols and constraints, got AsSelect".into(),
715          ));
716        };
717
718        let mut foreign_keys: Vec<ForeignKey> = vec![];
719        let mut unique: Vec<UniqueConstraint> = vec![];
720        let mut checks: Vec<Check> = vec![];
721
722        for constraint in constraints.unwrap_or_default() {
723          match constraint.constraint {
724            TableConstraint::ForeignKey {
725              columns,
726              clause,
727              deref_clause,
728            } => {
729              foreign_keys.push(build_foreign_key(
730                constraint.name,
731                Some(columns),
732                clause,
733                deref_clause,
734              ));
735            }
736            TableConstraint::Unique {
737              columns,
738              conflict_clause,
739            } => {
740              unique.push(UniqueConstraint {
741                name: constraint.name.map(unquote_name),
742                columns: columns.into_iter().map(|c| unquote_expr(c.expr)).collect(),
743                conflict_clause: conflict_clause.map(|c| c.into()),
744              });
745            }
746            TableConstraint::Check(expr) => {
747              checks.push(Check {
748                name: constraint.name.map(unquote_name),
749                expr: expr.to_string(),
750              });
751            }
752            TableConstraint::PrimaryKey { .. } => {
753              warn!("PK table constraint not implemented. Use column constraints.");
754            }
755          }
756        }
757
758        let columns: Vec<Column> = columns
759          .into_iter()
760          .map(|(name, def): (Name, ColumnDefinition)| {
761            let ColumnDefinition {
762              col_name,
763              col_type,
764              constraints,
765              flags: _,
766            } = def;
767            assert_eq!(name, col_name);
768
769            let name = unquote_name(col_name);
770            assert!(!name.is_empty());
771
772            let data_type: ColumnDataType = match col_type {
773              Some(x) => x.into(),
774              None => ColumnDataType::Null,
775            };
776
777            let options: Vec<ColumnOption> = constraints
778              .into_iter()
779              .map(|named_constraint| named_constraint.constraint.into())
780              .collect();
781
782            return Column {
783              name,
784              data_type,
785              options,
786            };
787          })
788          .collect();
789
790        Ok(Table {
791          name: tbl_name.into(),
792          strict: flags.contains(TabFlags::Strict),
793          columns,
794          foreign_keys,
795          unique,
796          checks,
797          virtual_table: false,
798          temporary,
799        })
800      }
801      Stmt::CreateVirtualTable {
802        tbl_name,
803        args: _args,
804        ..
805      } => Ok(Table {
806        name: tbl_name.into(),
807        strict: false,
808        columns: vec![],
809        foreign_keys: vec![],
810        unique: vec![],
811        checks: vec![],
812        virtual_table: true,
813        temporary: false,
814      }),
815      _ => Err(SchemaError::Precondition(
816        format!("expected 'CREATE [VIRTUAL] TABLE', got: {value:?}").into(),
817      )),
818    };
819  }
820}
821
822impl From<sqlite3_parser::ast::Type> for ColumnDataType {
823  fn from(data_type: sqlite3_parser::ast::Type) -> Self {
824    return ColumnDataType::from_type_name(&data_type.name).unwrap_or(ColumnDataType::Null);
825  }
826}
827
828impl From<sqlite3_parser::ast::ColumnConstraint> for ColumnOption {
829  fn from(constraint: sqlite3_parser::ast::ColumnConstraint) -> Self {
830    type Constraint = sqlite3_parser::ast::ColumnConstraint;
831
832    return match constraint {
833      Constraint::PrimaryKey {
834        conflict_clause,
835        order: _,
836        auto_increment: _,
837      } => ColumnOption::Unique {
838        is_primary: true,
839        conflict_clause: conflict_clause.map(|c| c.into()),
840      },
841      Constraint::Unique(conflict_clause) => ColumnOption::Unique {
842        is_primary: false,
843        conflict_clause: conflict_clause.map(|c| c.into()),
844      },
845      Constraint::Check(expr) => {
846        // NOTE: This is not using unquote on purpose, since this is not an identifier.
847        ColumnOption::Check(expr.to_string())
848      }
849      Constraint::ForeignKey {
850        clause,
851        deref_clause,
852      } => {
853        let fk = build_foreign_key(None, None, clause, deref_clause);
854
855        ColumnOption::ForeignKey {
856          foreign_table: fk.foreign_table,
857          referred_columns: fk.referred_columns,
858          on_delete: fk.on_delete,
859          on_update: fk.on_update,
860        }
861      }
862      Constraint::NotNull { .. } => ColumnOption::NotNull,
863      Constraint::Default(expr) => {
864        // NOTE: This is not using unquote on purpose to avoid turning "DEFAULT ''" into "DEFAULT".
865        ColumnOption::Default(expr.to_string())
866      }
867      Constraint::Generated { expr, typ } => ColumnOption::Generated {
868        // NOTE: This is not using unquote on purpose to avoid turning "AS ('')" into "AS ()".
869        expr: expr.to_string(),
870        mode: typ.and_then(|t| match &*t.0 {
871          "VIRTUAL" => Some(GeneratedExpressionMode::Virtual),
872          "STORED" => Some(GeneratedExpressionMode::Stored),
873          x => {
874            warn!("Unexpected generated column mode: {x}");
875            None
876          }
877        }),
878      },
879      Constraint::Collate { .. } | Constraint::Defer(_) => {
880        panic!("Not implemented: {constraint:?}");
881      }
882    };
883  }
884}
885
886impl TryFrom<sqlite3_parser::ast::Stmt> for TableIndex {
887  type Error = SchemaError;
888
889  fn try_from(value: sqlite3_parser::ast::Stmt) -> Result<Self, Self::Error> {
890    return match value {
891      sqlite3_parser::ast::Stmt::CreateIndex {
892        unique,
893        if_not_exists,
894        idx_name,
895        tbl_name,
896        columns,
897        where_clause,
898      } => Ok(TableIndex {
899        name: idx_name.into(),
900        table_name: unquote_name(tbl_name),
901        columns: columns
902          .into_iter()
903          .map(|order_expr| ColumnOrder {
904            column_name: unquote_expr(order_expr.expr),
905            ascending: order_expr
906              .order
907              .map(|order| order == sqlite3_parser::ast::SortOrder::Asc),
908            nulls_first: order_expr
909              .nulls
910              .map(|order| order == sqlite3_parser::ast::NullsOrder::First),
911          })
912          .collect(),
913        unique,
914        predicate: where_clause.map(|clause| {
915          // NOTE: this is deliberately not unquoting.
916          clause.to_string()
917        }),
918        if_not_exists,
919      }),
920      _ => Err(SchemaError::Precondition(
921        format!("expected 'CREATE INDEX', got: {value:?}").into(),
922      )),
923    };
924  }
925}
926
927struct SelectFormatter(sqlite3_parser::ast::Select);
928
929impl std::fmt::Display for SelectFormatter {
930  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
931    self.0.to_fmt(f)
932  }
933}
934
935impl View {
936  pub fn from(value: sqlite3_parser::ast::Stmt, tables: &[Table]) -> Result<Self, SchemaError> {
937    return match value {
938      sqlite3_parser::ast::Stmt::CreateView {
939        temporary,
940        if_not_exists,
941        view_name,
942        columns,
943        select,
944      } => {
945        let columns = match columns.is_some() {
946          true => {
947            info!("CREATE VIEW column filtering not supported (yet)");
948            None
949          }
950          false => try_extract_column_mapping((*select).clone(), tables)?.map(|column_mapping| {
951            column_mapping
952              .into_iter()
953              .map(|mapping| mapping.column)
954              .collect()
955          }),
956        };
957
958        Ok(View {
959          name: view_name.into(),
960          columns,
961          query: SelectFormatter(*select).to_string(),
962          temporary,
963          if_not_exists,
964        })
965      }
966      _ => Err(SchemaError::Precondition(
967        format!("expected 'CREATE VIEW', got: {value:?}").into(),
968      )),
969    };
970  }
971}
972
973fn to_entry(
974  qn: AstQualifiedName,
975  alias: Option<sqlite3_parser::ast::As>,
976) -> (String, QualifiedName) {
977  return (
978    alias
979      .and_then(|alias| {
980        if let sqlite3_parser::ast::As::As(name) = alias {
981          return Some(unquote_name(name));
982        }
983        None
984      })
985      .unwrap_or_else(|| qn.to_string()),
986    qn.into(),
987  );
988}
989
990#[derive(Clone, Debug)]
991#[allow(unused)]
992struct ReferredColumn {
993  table_name: QualifiedName,
994  column_name: String,
995}
996
997#[derive(Clone, Debug)]
998struct ColumnMapping {
999  column: Column,
1000
1001  #[allow(unused)]
1002  referred_column: Option<ReferredColumn>,
1003}
1004
1005fn try_extract_column_mapping(
1006  select: sqlite3_parser::ast::Select,
1007  tables: &[Table],
1008) -> Result<Option<Vec<ColumnMapping>>, SchemaError> {
1009  let body = select.body;
1010
1011  if body.compounds.is_some() {
1012    return Ok(None);
1013  }
1014
1015  let sqlite3_parser::ast::OneSelect::Select {
1016    columns,
1017    distinctness,
1018    from,
1019    group_by,
1020    having: _,
1021    where_clause: _,
1022    window_clause,
1023  } = body.select
1024  else {
1025    return Ok(None);
1026  };
1027
1028  if distinctness.is_some() || group_by.is_some() || window_clause.is_some() {
1029    return Ok(None);
1030  }
1031
1032  // First build list of referenced tables and their aliases.
1033  let Some(FromClause { select, joins, .. }) = from else {
1034    return Ok(None);
1035  };
1036  let Some(select) = select else {
1037    return Ok(None);
1038  };
1039  let SelectTable::Table(fqn, alias, _indexed) = *select else {
1040    return Ok(None);
1041  };
1042
1043  // Use IndexMap to preserve insertion order.
1044  let mut table_names = indexmap::IndexMap::<String, QualifiedName>::from([to_entry(fqn, alias)]);
1045
1046  if let Some(joins) = joins {
1047    for join in joins {
1048      let SelectTable::Table(fqn, alias, _indexed) = join.table else {
1049        return Ok(None);
1050      };
1051
1052      let entry = to_entry(fqn, alias);
1053      table_names.insert(entry.0, entry.1);
1054    }
1055  }
1056
1057  // Now we should have a map of all involved tables and their aliases (if any).
1058  let all_tables: HashMap<QualifiedName, &Table> =
1059    tables.iter().map(|t| (t.name.clone(), t)).collect();
1060  let mut all_columns = HashMap::<String, (&Table, &Column)>::new();
1061
1062  // Make sure we know all tables and all tables are strict.
1063  for table_name in table_names.values() {
1064    match all_tables.get(table_name) {
1065      Some(table) => {
1066        if !table.strict {
1067          info!("Skipping view: referenced table: {table_name:?} not strict");
1068          return Ok(None);
1069        }
1070
1071        for col in &table.columns {
1072          all_columns.insert(col.name.clone(), (table, col));
1073        }
1074      }
1075      None => {
1076        return Err(SchemaError::Precondition(
1077          format!("View's SELECT references missing table: {table_name:?}").into(),
1078        ));
1079      }
1080    };
1081  }
1082
1083  let mut mapping: Vec<ColumnMapping> = vec![];
1084  for col in columns {
1085    use sqlite3_parser::ast::Expr;
1086    use sqlite3_parser::ast::ResultColumn;
1087
1088    match col {
1089      ResultColumn::Star => {
1090        for table_name in table_names.values() {
1091          let table = all_tables.get(table_name).expect("checked above");
1092          for c in &table.columns {
1093            mapping.push(ColumnMapping {
1094              column: c.clone(),
1095              referred_column: Some(ReferredColumn {
1096                table_name: table.name.clone(),
1097                column_name: c.name.clone(),
1098              }),
1099            });
1100          }
1101        }
1102      }
1103      ResultColumn::TableStar(name) => {
1104        let name = unquote_name(name);
1105        let Some(table_name) = table_names.get(&name) else {
1106          return Err(SchemaError::Precondition(
1107            format!("Missing alias: {name}").into(),
1108          ));
1109        };
1110
1111        let table = all_tables.get(table_name).expect("checked above");
1112        for c in &table.columns {
1113          mapping.push(ColumnMapping {
1114            column: c.clone(),
1115            referred_column: Some(ReferredColumn {
1116              table_name: table.name.clone(),
1117              column_name: c.name.clone(),
1118            }),
1119          });
1120        }
1121      }
1122      ResultColumn::Expr(expr, alias) => match expr {
1123        Expr::Id(id) => {
1124          let col_name = unquote_id(id.clone());
1125          let Some((table, column)) = all_columns.get(&col_name) else {
1126            return Err(SchemaError::Precondition(
1127              format!("Missing columns: {id:?}").into(),
1128            ));
1129          };
1130
1131          let name = alias
1132            .and_then(|alias| {
1133              if let sqlite3_parser::ast::As::As(name) = alias {
1134                return Some(unquote_name(name));
1135              }
1136              None
1137            })
1138            .unwrap_or_else(|| column.name.clone());
1139
1140          mapping.push(ColumnMapping {
1141            column: Column {
1142              name,
1143              data_type: column.data_type,
1144              options: column.options.clone(),
1145            },
1146            referred_column: Some(ReferredColumn {
1147              table_name: table.name.clone(),
1148              column_name: column.name.clone(),
1149            }),
1150          });
1151        }
1152        Expr::Qualified(qualifier, name) => {
1153          let qualifier = unquote_name(qualifier);
1154          let col_name = unquote_name(name);
1155
1156          let Some(table_name) = table_names.get(&qualifier) else {
1157            return Err(SchemaError::Precondition(
1158              format!("Missing table with qualifier: {qualifier}").into(),
1159            ));
1160          };
1161
1162          let table = all_tables.get(table_name).expect("checked above");
1163          let Some(column) = table.columns.iter().find(|c| c.name == col_name) else {
1164            return Err(SchemaError::Precondition(
1165              format!("Missing col: {col_name}").into(),
1166            ));
1167          };
1168
1169          let name = alias
1170            .and_then(|alias| {
1171              if let sqlite3_parser::ast::As::As(name) = alias {
1172                return Some(unquote_name(name));
1173              }
1174              None
1175            })
1176            .unwrap_or_else(|| column.name.clone());
1177
1178          mapping.push(ColumnMapping {
1179            column: Column {
1180              name,
1181              data_type: column.data_type,
1182              options: column.options.clone(),
1183            },
1184            referred_column: Some(ReferredColumn {
1185              table_name: table.name.clone(),
1186              column_name: column.name.clone(),
1187            }),
1188          });
1189        }
1190        Expr::Cast { expr: _, type_name } => {
1191          let Some(type_name) = type_name else {
1192            return Err(SchemaError::Precondition(
1193              "Missing type_name in cast".into(),
1194            ));
1195          };
1196          let Some(data_type) = ColumnDataType::from_type_name(&type_name.name) else {
1197            return Err(SchemaError::Precondition(
1198              "Missing type_name in cast".into(),
1199            ));
1200          };
1201
1202          let Some(name) = alias.and_then(|alias| {
1203            if let sqlite3_parser::ast::As::As(name) = alias {
1204              return Some(unquote_name(name));
1205            }
1206            None
1207          }) else {
1208            return Err(SchemaError::Precondition("Missing alias in cast".into()));
1209          };
1210
1211          mapping.push(ColumnMapping {
1212            column: Column {
1213              name,
1214              data_type,
1215              options: vec![ColumnOption::Null],
1216            },
1217            referred_column: None,
1218          });
1219        }
1220        _x => {
1221          // We cannot map arbitrary expressions.
1222          #[cfg(debug_assertions)]
1223          debug!("skipping expr: {_x:?}");
1224
1225          return Ok(None);
1226        }
1227      },
1228    };
1229  }
1230
1231  return Ok(Some(mapping));
1232}
1233
1234fn build_foreign_key(
1235  name: Option<Name>,
1236  columns: Option<Vec<IndexedColumn>>,
1237  clause: ForeignKeyClause,
1238  deref_clause: Option<DeferSubclause>,
1239) -> ForeignKey {
1240  if let Some(ref clause) = deref_clause {
1241    // TOOD: Parse DEFERRABLE.
1242    warn!("Unsupported DEFERRABLE in FK clause: {clause:?}");
1243  }
1244
1245  let (on_update, on_delete) = unparse_fk_trigger(&clause.args);
1246
1247  return ForeignKey {
1248    name: name.map(unquote_name),
1249    foreign_table: unquote_name(clause.tbl_name.clone()),
1250    columns: columns
1251      .unwrap_or_default()
1252      .into_iter()
1253      .map(|c| unquote_name(c.col_name))
1254      .collect(),
1255    referred_columns: clause
1256      .columns
1257      .unwrap_or_default()
1258      .into_iter()
1259      .map(|c| unquote_name(c.col_name))
1260      .collect(),
1261    on_update,
1262    on_delete,
1263  };
1264}
1265
1266fn unparse_fk_trigger(
1267  args: &Vec<sqlite3_parser::ast::RefArg>,
1268) -> (Option<ReferentialAction>, Option<ReferentialAction>) {
1269  use sqlite3_parser::ast::RefArg;
1270
1271  let mut on_update: Option<ReferentialAction> = None;
1272  let mut on_delete: Option<ReferentialAction> = None;
1273
1274  for arg in args {
1275    match arg {
1276      RefArg::OnDelete(action) => {
1277        on_delete = Some((*action).into());
1278      }
1279      RefArg::OnUpdate(action) => {
1280        on_update = Some((*action).into());
1281      }
1282      RefArg::OnInsert(action) => {
1283        error!("Unexpected ON INSERT in FK clause: {action:?}");
1284      }
1285      RefArg::Match(name) => {
1286        // SQL supports FK MATCH clause, which is *not* supported by sqlite:
1287        //   https://www.sqlite.org/foreignkeys.html#fk_unsupported
1288        warn!("Unsupported MATCH in FK clause: {name:?}");
1289      }
1290    }
1291  }
1292
1293  return (on_update, on_delete);
1294}
1295
1296#[inline]
1297pub(crate) fn quote(column_names: &[String]) -> String {
1298  let mut s = String::new();
1299  for (i, name) in column_names.iter().enumerate() {
1300    if i > 0 {
1301      s.push_str(", '");
1302    } else {
1303      s.push('\'');
1304    }
1305    s.push_str(name);
1306    s.push('\'');
1307  }
1308  return s;
1309}
1310
1311#[inline]
1312fn unquote_string(s: &str) -> String {
1313  let n = s.as_bytes();
1314  if n.is_empty() {
1315    return String::new();
1316  }
1317
1318  return match n[0] {
1319    b'"' | b'`' | b'\'' | b'[' => {
1320      assert!(n.len() >= 2, "string: {s}");
1321      s[1..n.len() - 1].to_string()
1322    }
1323    _ => s.to_string(),
1324  };
1325}
1326
1327fn unquote_name(name: Name) -> String {
1328  return unquote_string(&name.0);
1329}
1330
1331fn unquote_qualified(name: AstQualifiedName) -> String {
1332  return unquote_name(name.name);
1333}
1334
1335fn unquote_db_name(name: &AstQualifiedName) -> Option<String> {
1336  return name.db_name.clone().map(unquote_name);
1337}
1338
1339fn unquote_id(id: sqlite3_parser::ast::Id) -> String {
1340  return unquote_string(&id.0);
1341}
1342
1343fn unquote_expr(expr: Expr) -> String {
1344  return match expr {
1345    Expr::Name(n) => unquote_name(n),
1346    Expr::Id(id) => unquote_id(id),
1347    Expr::Literal(Literal::String(s)) => unquote_string(&s),
1348    x => x.to_string(),
1349  };
1350}
1351
1352#[cfg(test)]
1353pub fn lookup_and_parse_table_schema(
1354  conn: &rusqlite::Connection,
1355  table_name: &str,
1356) -> anyhow::Result<Table> {
1357  const SQLITE_SCHEMA_TABLE: &str = "main.sqlite_schema";
1358
1359  let sql: String = conn.query_row(
1360    &format!("SELECT sql FROM {SQLITE_SCHEMA_TABLE} WHERE type = 'table' AND name = $1"),
1361    rusqlite::params!(table_name),
1362    |row| row.get(0),
1363  )?;
1364
1365  let Some(stmt) = sqlite3_parse_into_statement(&sql)? else {
1366    anyhow::bail!("Not a statement");
1367  };
1368
1369  return Ok(stmt.try_into()?);
1370}
1371
1372#[cfg(test)]
1373mod tests {
1374  use super::*;
1375
1376  #[test]
1377  fn test_quote() {
1378    assert_eq!("", quote(&vec![]));
1379    assert_eq!("''", quote(&vec!["".to_string()]));
1380    assert_eq!("'foo', ''", quote(&vec!["foo".to_string(), "".to_string()]));
1381  }
1382
1383  #[test]
1384  fn test_unquote() {
1385    assert_eq!(unquote_name(Name("".into())), "");
1386    assert_eq!(unquote_name(Name("['``']".into())), "'``'");
1387    assert_eq!(unquote_name(Name("\"[]\"".into())), "[]");
1388  }
1389
1390  #[test]
1391  fn test_create_table_statement_quoting() {
1392    let table_name = QualifiedName {
1393      name: "table".to_string(),
1394      database_schema: None,
1395    };
1396    let statement = format!(
1397      r#"
1398      CREATE TABLE {table_name} (
1399          'index'       TEXT,
1400          `delete`      TEXT,
1401          [create]      TEXT
1402      ) STRICT;
1403      "#,
1404      table_name = table_name.escaped_string(),
1405    );
1406
1407    let parsed = sqlite3_parse_into_statement(&statement).unwrap().unwrap();
1408
1409    let table: Table = parsed.try_into().unwrap();
1410    assert_eq!(table.name, table_name);
1411    let sql = table.create_table_statement();
1412
1413    assert_eq!(
1414      "CREATE TABLE \"table\" ('index' TEXT, 'delete' TEXT, 'create' TEXT) STRICT",
1415      sql
1416    );
1417    sqlite3_parse_into_statement(&sql).unwrap().unwrap();
1418  }
1419
1420  struct StmtFormatter(Stmt);
1421
1422  impl std::fmt::Display for StmtFormatter {
1423    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1424      self.0.to_fmt(f)
1425    }
1426  }
1427
1428  #[tokio::test]
1429  async fn test_statement_to_table_schema_and_back() {
1430    let statement = format!(
1431      r#"
1432      CREATE TABLE test (
1433          -- Comment
1434          id                           BLOB PRIMARY KEY DEFAULT (uuid_v7()) NOT NULL,
1435          user                         BLOB DEFAULT '' REFERENCES 'table'(`index`) ON DELETE CASCADE,
1436          user_id                      BLOB,
1437          email                        TEXT NOT NULL,
1438          email_visibility             INTEGER DEFAULT FALSE NOT NULL,
1439          username                     TEXT UNIQUE ON CONFLICT ABORT,
1440          age                          INTEGER CHECK(age >= 0),
1441          double_age                   INTEGER GENERATED ALWAYS AS (2 * 'age') VIRTUAL,
1442          triple_age                   INTEGER AS (3 * age) STORED,
1443          gen_text                     TEXT AS ('') VIRTUAL,
1444          [index]                      TEXT,
1445
1446          UNIQUE (email),
1447          -- optional constraint name:
1448          CONSTRAINT `unique` UNIQUE ([index]) ON CONFLICT FAIL,
1449          FOREIGN KEY(user_id) REFERENCES 'table'('index') ON DELETE CASCADE,
1450          CONSTRAINT `check` CHECK(username != '')
1451      ) STRICT;
1452      "#
1453    );
1454
1455    {
1456      // First Make sure the query is actually valid, as opposed to "only" parsable.
1457      let conn = trailbase_extension::connect_sqlite(None, None).unwrap();
1458      conn.execute(&statement, ()).unwrap();
1459    }
1460
1461    let statement1 = sqlite3_parse_into_statement(&statement).unwrap().unwrap();
1462    let table1: Table = statement1.clone().try_into().unwrap();
1463
1464    let sql = table1.create_table_statement();
1465    {
1466      // Same as above, make sure the constructed query is valid as opposed to "only" parsable.
1467      let conn = trailbase_extension::connect_sqlite(None, None).unwrap();
1468      conn.execute(&sql, ()).unwrap();
1469    }
1470
1471    let statement2 = sqlite3_parse_into_statement(&sql).unwrap().unwrap();
1472
1473    let table2: Table = statement2.clone().try_into().unwrap();
1474
1475    // NOTE: Ideally we'd just compare the parsed sqlite3_parser ASTs, however it doesn't properly
1476    // parse out escape characters, so `statement1` and `statement2` will be escaped differently.
1477    // So we're matching on strings instead with all quoting removed.
1478    // assert_eq!(statement1, statement2, "Got: {sql2}\nExpected: {sql1}");
1479    let pattern = ['\'', '"', '[', ']', '`'];
1480    let sql2 = StmtFormatter(statement2.clone())
1481      .to_string()
1482      .replace(&pattern, "");
1483    let sql1 = StmtFormatter(statement1.clone())
1484      .to_string()
1485      .replace(&pattern, "");
1486    assert_eq!(sql2, sql1, "Got: {sql2}\nExpected: {sql1}");
1487
1488    assert_eq!(table1, table2, "generated stmt: {sql}");
1489  }
1490
1491  #[test]
1492  fn test_statement_to_table_index_and_back() {
1493    const SQL: &str =
1494      "CREATE UNIQUE INDEX IF NOT EXISTS 'index' ON 'table' ('create') WHERE 'create' != '';";
1495
1496    let statement1 = sqlite3_parse_into_statement(SQL).unwrap().unwrap();
1497    let index1: TableIndex = statement1.clone().try_into().unwrap();
1498
1499    let statement2 = sqlite3_parse_into_statement(&index1.create_index_statement())
1500      .unwrap()
1501      .unwrap();
1502    let index2: TableIndex = statement2.clone().try_into().unwrap();
1503
1504    assert_eq!(statement1, statement2);
1505    assert_eq!(index1, index2);
1506  }
1507
1508  #[test]
1509  fn test_parse_create_trigger() {
1510    const SQL: &str = r#"
1511      CREATE TRIGGER cust_addr_chng
1512      INSTEAD OF UPDATE OF cust_addr ON customer_address
1513      FOR EACH ROW
1514      BEGIN
1515        UPDATE customer SET cust_addr=NEW.cust_addr WHERE cust_id=NEW.cust_id;
1516      END
1517    "#;
1518
1519    sqlite3_parse_into_statement(SQL).unwrap().unwrap();
1520  }
1521
1522  #[test]
1523  fn test_parse_create_index() {
1524    let sql =
1525      r#"CREATE UNIQUE INDEX "main"."index_name" ON 'table_name' (a ASC, b DESC) WHERE x > 0"#;
1526    let stmt = sqlite3_parse_into_statement(sql).unwrap().unwrap();
1527    let index: TableIndex = stmt.try_into().unwrap();
1528
1529    let sql1 = index.create_index_statement();
1530    let stmt1 = sqlite3_parse_into_statement(&sql1).unwrap().unwrap();
1531    let index1: TableIndex = stmt1.try_into().unwrap();
1532
1533    assert_eq!(index, index1, "Parsed: {sql1}");
1534  }
1535
1536  #[test]
1537  fn test_view_column_extraction() {
1538    let sql = "SELECT user, *, a.*, p.user AS foo FROM foo.articles AS a LEFT JOIN bar.profiles AS p ON p.user = a.author";
1539    let sqlite3_parser::ast::Stmt::Select(select) =
1540      sqlite3_parse_into_statement(sql).unwrap().unwrap()
1541    else {
1542      panic!("Not a select");
1543    };
1544
1545    let tables = vec![
1546      Table {
1547        name: QualifiedName {
1548          name: "profiles".to_string(),
1549          database_schema: Some("bar".to_string()),
1550        },
1551        strict: true,
1552        columns: vec![
1553          Column {
1554            name: "user".to_string(),
1555            data_type: ColumnDataType::Blob,
1556            options: vec![
1557              ColumnOption::Unique {
1558                is_primary: true,
1559                conflict_clause: None,
1560              },
1561              ColumnOption::ForeignKey {
1562                foreign_table: "_user".to_string(),
1563                referred_columns: vec!["id".to_string()],
1564                on_delete: None,
1565                on_update: None,
1566              },
1567            ],
1568          },
1569          Column {
1570            name: "username".to_string(),
1571            data_type: ColumnDataType::Text,
1572            options: vec![],
1573          },
1574        ],
1575        foreign_keys: vec![],
1576        unique: vec![],
1577        checks: vec![],
1578        virtual_table: false,
1579        temporary: false,
1580      },
1581      Table {
1582        name: QualifiedName {
1583          name: "articles".to_string(),
1584          database_schema: Some("foo".to_string()),
1585        },
1586        strict: true,
1587        columns: vec![
1588          Column {
1589            name: "id".to_string(),
1590            data_type: ColumnDataType::Blob,
1591            options: vec![ColumnOption::Unique {
1592              is_primary: true,
1593              conflict_clause: None,
1594            }],
1595          },
1596          Column {
1597            name: "author".to_string(),
1598            data_type: ColumnDataType::Blob,
1599            options: vec![ColumnOption::ForeignKey {
1600              foreign_table: "_user".to_string(),
1601              referred_columns: vec!["id".to_string()],
1602              on_delete: None,
1603              on_update: None,
1604            }],
1605          },
1606          Column {
1607            name: "body".to_string(),
1608            data_type: ColumnDataType::Text,
1609            options: vec![],
1610          },
1611        ],
1612        foreign_keys: vec![],
1613        unique: vec![],
1614        checks: vec![],
1615        virtual_table: false,
1616        temporary: false,
1617      },
1618    ];
1619
1620    let mapping = try_extract_column_mapping(*select, &tables)
1621      .unwrap()
1622      .unwrap();
1623
1624    assert_eq!(
1625      mapping
1626        .iter()
1627        .map(|m| m.referred_column.as_ref().unwrap().column_name.as_str())
1628        .collect::<Vec<_>>(),
1629      [
1630        "user", "id", "author", "body", "user", "username", "id", "author", "body", "user"
1631      ]
1632    );
1633
1634    assert_eq!(
1635      mapping
1636        .iter()
1637        .map(|m| m.column.name.as_str())
1638        .collect::<Vec<_>>(),
1639      [
1640        "user", "id", "author", "body", "user", "username", "id", "author", "body", "foo"
1641      ]
1642    );
1643  }
1644}