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 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 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#[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 pub on_delete: Option<ReferentialAction>,
107 pub on_update: Option<ReferentialAction>,
108 }
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#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
157pub struct UniqueConstraint {
158 pub name: Option<String>,
159
160 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#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
196pub enum ConflictResolution {
197 Rollback,
199 Abort, Fail,
203 Ignore,
205 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 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 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 Unique {
283 is_primary: bool,
284 conflict_clause: Option<ConflictResolution>,
285 },
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 Any,
358 Blob,
359 Text,
360 Integer,
361 Real,
362 Numeric, #[allow(clippy::upper_case_acronyms)]
366 JSON,
367 #[allow(clippy::upper_case_acronyms)]
368 JSONB,
369
370 Int,
374 TinyInt,
375 SmallInt,
376 MediumInt,
377 BigInt,
378 UnignedBigInt,
379 Int2,
380 Int4,
381 Int8,
382
383 Character,
385 Varchar,
386 VaryingCharacter,
387 NChar,
388 NativeCharacter,
389 NVarChar,
390 Clob,
391
392 Double,
394 DoublePrecision,
395 Float,
396
397 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" => ColumnDataType::JSON,
417 "JSONB" => ColumnDataType::JSONB,
418
419 "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 "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 "DOUBLE" => ColumnDataType::Double,
443 "DOUBLE PRECISION" => ColumnDataType::DoublePrecision,
444 "FLOAT" => ColumnDataType::Float,
445
446 "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 pub columns: Vec<Column>,
587
588 pub foreign_keys: Vec<ForeignKey>,
591 pub unique: Vec<UniqueConstraint>,
592 pub checks: Vec<Check>,
593
594 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 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 column_defs_and_table_constraints.extend(self.unique.iter().map(|unique| unique.to_fragment()));
612
613 column_defs_and_table_constraints.extend(self.foreign_keys.iter().map(|fk| fk.to_fragment()));
615
616 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 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 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 ColumnOption::Default(expr.to_string())
866 }
867 Constraint::Generated { expr, typ } => ColumnOption::Generated {
868 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 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 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 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 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 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 #[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 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 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 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 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 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}