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