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