1use super::ddl::{
7 Column, ForeignKey, Index, IndexColumn, IndexOrigin, PrimaryKey, SqliteEntity, Table,
8 UniqueConstraint, View,
9};
10use super::ddl::{GeneratedType, ParsedGenerated};
11use super::snapshot::SQLiteSnapshot;
12use std::collections::{BTreeMap, HashMap, HashSet};
13
14#[derive(Debug, Clone)]
16pub struct IntrospectError {
17 pub message: String,
18 pub table: Option<String>,
19}
20
21impl std::fmt::Display for IntrospectError {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 if let Some(table) = &self.table {
24 write!(f, "Introspection error for '{}': {}", table, self.message)
25 } else {
26 write!(f, "Introspection error: {}", self.message)
27 }
28 }
29}
30
31impl std::error::Error for IntrospectError {}
32
33pub type IntrospectResult<T> = Result<T, IntrospectError>;
35
36#[derive(Debug, Clone)]
38pub struct RawColumnInfo {
39 pub table: String,
40 pub cid: i32,
41 pub name: String,
42 pub column_type: String,
43 pub not_null: bool,
44 pub default_value: Option<String>,
45 pub pk: i32,
46 pub hidden: i32,
47 pub sql: Option<String>,
48}
49
50#[derive(Debug, Clone)]
52pub struct RawIndexInfo {
53 pub table: String,
54 pub name: String,
55 pub unique: bool,
56 pub origin: String, pub partial: bool,
58}
59
60#[derive(Debug, Clone)]
62pub struct RawIndexColumn {
63 pub index_name: String,
64 pub seqno: i32,
65 pub cid: i32,
66 pub name: Option<String>,
67 pub desc: bool,
68 pub coll: String,
69 pub key: bool,
70}
71
72#[derive(Debug, Clone)]
74pub struct RawForeignKey {
75 pub table: String,
76 pub id: i32,
77 pub seq: i32,
78 pub to_table: String,
79 pub from_column: String,
80 pub to_column: String,
81 pub on_update: String,
82 pub on_delete: String,
83 pub r#match: String,
84}
85
86#[derive(Debug, Clone)]
88pub struct RawViewInfo {
89 pub name: String,
90 pub sql: String,
91}
92
93#[derive(Debug, Clone, Default)]
98pub struct RawIntrospection {
99 pub tables: Vec<(String, Option<String>)>,
100 pub columns: Vec<RawColumnInfo>,
101 pub indexes: Vec<RawIndexInfo>,
102 pub index_columns: Vec<RawIndexColumn>,
103 pub foreign_keys: Vec<RawForeignKey>,
104 pub views: Vec<RawViewInfo>,
105 pub index_sql: Vec<(String, String)>,
109}
110
111#[must_use]
113pub fn assemble_ddl(raw: RawIntrospection) -> super::SQLiteDDL {
114 let table_sql_map: HashMap<String, String> = raw
115 .tables
116 .iter()
117 .filter_map(|(name, sql)| sql.as_ref().map(|sql| (name.clone(), sql.clone())))
118 .collect();
119
120 let mut generated_columns = HashMap::<String, ParsedGenerated>::new();
121 for (table, sql) in &table_sql_map {
122 generated_columns.extend(parse_generated_columns_from_table_sql(table, sql));
123 }
124 let primary_key_columns: HashSet<(String, String)> = raw
125 .columns
126 .iter()
127 .filter(|column| column.pk > 0)
128 .map(|column| (column.table.clone(), column.name.clone()))
129 .collect();
130
131 let (columns, primary_keys) =
132 process_columns(&raw.columns, &generated_columns, &primary_key_columns);
133 let index_sql_map: HashMap<String, String> = raw.index_sql.iter().cloned().collect();
134 let indexes = process_indexes_with_sql(&raw.indexes, &raw.index_columns, &index_sql_map);
135 let foreign_keys = process_foreign_keys(&raw.foreign_keys);
136 let unique_constraints =
137 process_unique_constraints_from_indexes(&raw.indexes, &raw.index_columns);
138
139 let mut ddl = super::SQLiteDDL::new();
140 for (table_name, table_sql) in raw.tables {
141 let mut table = Table::new(table_name);
142 if let Some(sql) = table_sql {
143 let (strict, without_rowid) = parse_table_options(&sql);
144 table.strict = strict;
145 table.without_rowid = without_rowid;
146 }
147 ddl.tables.push(table);
148 }
149 for column in columns {
150 ddl.columns.push(column);
151 }
152 for index in indexes {
153 ddl.indexes.push(index);
154 }
155 for foreign_key in foreign_keys {
156 ddl.fks.push(foreign_key);
157 }
158 for primary_key in primary_keys {
159 ddl.pks.push(primary_key);
160 }
161 for unique_constraint in unique_constraints {
162 ddl.uniques.push(unique_constraint);
163 }
164
165 for raw_view in raw.views {
166 let mut view = View::new(raw_view.name);
167 if let Some(definition) = parse_view_sql(&raw_view.sql) {
168 view.definition = Some(definition.into());
169 } else {
170 view.error = Some("Failed to parse view SQL".into());
171 }
172 ddl.views.push(view);
173 }
174
175 ddl
176}
177
178pub type EntityFilter = Box<dyn Fn(&str, &str) -> bool>;
180
181pub fn default_filter() -> EntityFilter {
183 Box::new(|_entity_type, _name| true)
184}
185
186#[must_use]
188pub fn system_table_filter(name: &str) -> bool {
189 !name.starts_with("sqlite_")
190 && !name.starts_with("_cf_")
191 && !name.starts_with("_litestream_")
192 && !name.starts_with("libsql_")
193 && !name.starts_with("d1_")
194 && name != "__drizzle_migrations"
195}
196
197#[derive(Debug, Clone, Default)]
199pub struct IntrospectionResult {
200 pub tables: Vec<Table>,
201 pub columns: Vec<Column>,
202 pub indexes: Vec<Index>,
203 pub foreign_keys: Vec<ForeignKey>,
204 pub primary_keys: Vec<PrimaryKey>,
205 pub unique_constraints: Vec<UniqueConstraint>,
206 pub views: Vec<View>,
207 pub errors: Vec<IntrospectError>,
208}
209
210impl IntrospectionResult {
211 #[must_use]
213 pub fn to_snapshot(&self) -> SQLiteSnapshot {
214 let mut snapshot = SQLiteSnapshot::new();
215
216 for table in &self.tables {
217 snapshot.add_entity(SqliteEntity::Table(table.clone()));
218 }
219 for column in &self.columns {
220 snapshot.add_entity(SqliteEntity::Column(column.clone()));
221 }
222 for index in &self.indexes {
223 snapshot.add_entity(SqliteEntity::Index(index.clone()));
224 }
225 for fk in &self.foreign_keys {
226 snapshot.add_entity(SqliteEntity::ForeignKey(fk.clone()));
227 }
228 for pk in &self.primary_keys {
229 snapshot.add_entity(SqliteEntity::PrimaryKey(pk.clone()));
230 }
231 for unique in &self.unique_constraints {
232 snapshot.add_entity(SqliteEntity::UniqueConstraint(unique.clone()));
233 }
234 for view in &self.views {
235 snapshot.add_entity(SqliteEntity::View(view.clone()));
236 }
237
238 snapshot
239 }
240
241 #[must_use]
243 pub const fn has_errors(&self) -> bool {
244 !self.errors.is_empty()
245 }
246}
247
248#[must_use]
250pub fn process_columns<S1: std::hash::BuildHasher, S2: std::hash::BuildHasher>(
251 raw_columns: &[RawColumnInfo],
252 generated_columns: &std::collections::HashMap<String, super::ddl::ParsedGenerated, S1>,
253 _pk_columns: &std::collections::HashSet<(String, String), S2>, ) -> (Vec<Column>, Vec<PrimaryKey>) {
255 let mut autoinc_by_table: HashMap<String, std::collections::HashSet<String>> = HashMap::new();
257 for c in raw_columns {
258 if autoinc_by_table.contains_key(&c.table) {
259 continue;
260 }
261 let Some(sql) = c.sql.as_deref() else {
262 continue;
263 };
264 autoinc_by_table.insert(
265 c.table.clone(),
266 parse_autoincrement_columns_from_table_sql(sql),
267 );
268 }
269
270 let columns: Vec<Column> = raw_columns
271 .iter()
272 .filter(|c| c.hidden != 1)
277 .map(|c| {
278 let key = format!("{}:{}", c.table, c.name);
279 let generated = generated_columns.get(&key).map(|g| super::ddl::Generated {
280 expression: g.expression.clone().into(),
281 gen_type: g.gen_type,
282 });
283
284 let is_autoincrement = autoinc_by_table
285 .get(&c.table)
286 .is_some_and(|set| set.contains(&c.name));
287
288 Column {
289 table: c.table.clone().into(),
290 name: c.name.clone().into(),
291 sql_type: normalize_sql_type(&c.column_type).into(),
292 not_null: c.not_null,
293 autoincrement: if is_autoincrement { Some(true) } else { None },
294 primary_key: None, unique: None, default: c.default_value.clone().map(std::convert::Into::into),
297 generated,
298 collate: None,
303 ordinal_position: Some(c.cid),
304 }
305 })
306 .collect();
307
308 let mut pk_map: BTreeMap<String, Vec<(i32, String)>> = BTreeMap::new();
314
315 for c in raw_columns.iter().filter(|c| c.pk > 0) {
316 pk_map
317 .entry(c.table.clone())
318 .or_default()
319 .push((c.pk, c.name.clone()));
320 }
321
322 let primary_keys: Vec<PrimaryKey> = pk_map
323 .into_iter()
324 .map(|(table, mut cols)| {
325 cols.sort_by_key(|(pk_pos, _)| *pk_pos);
326 let name = super::ddl::name_for_pk(&table);
327 PrimaryKey {
328 table: table.into(),
329 name: name.into(),
330 name_explicit: false,
331 columns: cols.into_iter().map(|(_, name)| name.into()).collect(),
332 }
333 })
334 .collect();
335
336 (columns, primary_keys)
337}
338
339fn normalize_sql_type(sql_type: &str) -> String {
341 sql_type.to_lowercase()
342}
343
344fn extract_table_body(sql: &str) -> Option<&str> {
349 let sql = sql.trim();
350 let start = sql.find('(')?;
351
352 let mut depth = 0i32;
353 let mut end: Option<usize> = None;
354 for (i, ch) in sql.char_indices().skip(start) {
355 match ch {
356 '(' => depth += 1,
357 ')' => {
358 depth -= 1;
359 if depth == 0 {
360 end = Some(i);
361 break;
362 }
363 }
364 _ => {}
365 }
366 }
367 Some(&sql[start + 1..end?])
368}
369
370fn table_options_tail(sql: &str) -> Option<&str> {
376 let start = sql.find('(')?;
377 let mut depth = 0i32;
378 for (i, ch) in sql.char_indices().skip(start) {
379 match ch {
380 '(' => depth += 1,
381 ')' => {
382 depth -= 1;
383 if depth == 0 {
384 return Some(&sql[i + ch.len_utf8()..]);
385 }
386 }
387 _ => {}
388 }
389 }
390 None
391}
392
393#[must_use]
398pub fn parse_table_options(sql: &str) -> (bool, bool) {
399 let Some(tail) = table_options_tail(sql) else {
400 return (false, false);
401 };
402 let upper = tail.to_uppercase();
403 let tokens: Vec<&str> = upper
404 .split(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_'))
405 .filter(|t| !t.is_empty())
406 .collect();
407 let strict = tokens.contains(&"STRICT");
408 let without_rowid = tokens
409 .windows(2)
410 .any(|w| w[0] == "WITHOUT" && w[1] == "ROWID");
411 (strict, without_rowid)
412}
413
414fn split_top_level_commas(body: &str) -> Vec<&str> {
416 let mut parts: Vec<&str> = Vec::new();
417 let mut part_start = 0usize;
418 let mut p_depth = 0i32;
419 for (i, ch) in body.char_indices() {
420 match ch {
421 '(' => p_depth += 1,
422 ')' => p_depth -= 1,
423 ',' if p_depth == 0 => {
424 parts.push(body[part_start..i].trim());
425 part_start = i + 1;
426 }
427 _ => {}
428 }
429 }
430 parts.push(body[part_start..].trim());
431 parts
432}
433
434fn is_table_level_constraint(upper: &str) -> bool {
437 upper.starts_with("CONSTRAINT ")
438 || upper.starts_with("PRIMARY ")
439 || upper.starts_with("UNIQUE ")
440 || upper.starts_with("CHECK ")
441 || upper.starts_with("FOREIGN ")
442}
443
444fn take_column_name(def: &str) -> Option<(String, &str)> {
449 let def = def.trim();
450 if let Some(r) = def.strip_prefix('"') {
451 let endq = r.find('"')?;
452 return Some((r[..endq].to_string(), r[endq + 1..].trim_start()));
453 }
454 if let Some(r) = def.strip_prefix('`') {
455 let endq = r.find('`')?;
456 return Some((r[..endq].to_string(), r[endq + 1..].trim_start()));
457 }
458 if let Some(r) = def.strip_prefix('[') {
459 let endq = r.find(']')?;
460 return Some((r[..endq].to_string(), r[endq + 1..].trim_start()));
461 }
462 let name = def.split_whitespace().next()?;
463 let rest = def[name.len()..].trim_start();
464 Some((name.to_string(), rest))
465}
466
467fn parse_autoincrement_columns_from_table_sql(sql: &str) -> std::collections::HashSet<String> {
471 let mut out = std::collections::HashSet::new();
472
473 let Some(body) = extract_table_body(sql) else {
474 return out;
475 };
476
477 for item in split_top_level_commas(body) {
478 if item.is_empty() {
479 continue;
480 }
481
482 let upper = item.to_uppercase();
483 if is_table_level_constraint(&upper) {
484 continue;
485 }
486
487 if !upper.contains("AUTOINCREMENT") {
488 continue;
489 }
490 if !(upper.contains("INTEGER") && upper.contains("PRIMARY") && upper.contains("KEY")) {
491 continue;
492 }
493
494 if let Some((col_name, _rest)) = take_column_name(item) {
495 out.insert(col_name);
496 }
497 }
498
499 out
500}
501
502#[derive(Debug, Clone, Default)]
504pub struct ParsedIndexSql {
505 pub columns: Vec<IndexColumn>,
507 pub where_clause: Option<String>,
509}
510
511fn is_bare_identifier(s: &str) -> bool {
513 !s.is_empty() && s.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
514}
515
516fn parse_index_item(item: &str) -> IndexColumn {
525 let expression = || IndexColumn {
526 value: item.trim().to_string().into(),
527 is_expression: true,
528 };
529
530 let Some((name, rest)) = take_column_name(item) else {
531 return expression();
532 };
533
534 let quoted = matches!(item.trim_start().chars().next(), Some('"' | '`' | '['));
537 if !quoted && !is_bare_identifier(&name) {
538 return expression();
539 }
540
541 let mut tokens = rest.split_whitespace();
543 loop {
544 match tokens.next().map(str::to_ascii_uppercase) {
545 None => break,
546 Some(t) if t == "ASC" || t == "DESC" => {}
547 Some(t) if t == "COLLATE" => {
548 if tokens.next().is_none() {
549 return expression();
550 }
551 }
552 Some(_) => return expression(),
553 }
554 }
555
556 IndexColumn {
557 value: name.into(),
558 is_expression: false,
559 }
560}
561
562#[must_use]
566pub fn parse_index_sql(sql: &str) -> ParsedIndexSql {
567 let mut parsed = ParsedIndexSql::default();
568
569 let mut in_quote: Option<char> = None;
572 let mut open: Option<usize> = None;
573 for (i, ch) in sql.char_indices() {
574 match (in_quote, ch) {
575 (Some(q), _) if quote_closer(q) == ch => in_quote = None,
576 (Some(_), _) => {}
577 (None, '\'' | '"' | '`' | '[') => in_quote = Some(ch),
578 (None, '(') => {
579 open = Some(i);
580 break;
581 }
582 _ => {}
583 }
584 }
585 let Some(open) = open else {
586 return parsed;
587 };
588
589 let mut depth = 0i32;
590 let mut close: Option<usize> = None;
591 let mut in_quote: Option<char> = None;
592 for (i, ch) in sql.char_indices().skip(open) {
593 match (in_quote, ch) {
594 (Some(q), _) if quote_closer(q) == ch => in_quote = None,
595 (Some(_), _) => {}
596 (None, '\'' | '"' | '`' | '[') => in_quote = Some(ch),
597 (None, '(') => depth += 1,
598 (None, ')') => {
599 depth -= 1;
600 if depth == 0 {
601 close = Some(i);
602 break;
603 }
604 }
605 _ => {}
606 }
607 }
608 let Some(close) = close else {
609 return parsed;
610 };
611
612 let body = &sql[open + 1..close];
613 parsed.columns = split_top_level_commas(body)
614 .into_iter()
615 .filter(|item| !item.is_empty())
616 .map(parse_index_item)
617 .collect();
618
619 let tail = &sql[close + 1..];
621 let mut in_quote: Option<char> = None;
622 let bytes = tail.as_bytes();
623 for (i, ch) in tail.char_indices() {
624 match (in_quote, ch) {
625 (Some(q), _) if quote_closer(q) == ch => in_quote = None,
626 (Some(_), _) => {}
627 (None, '\'' | '"' | '`' | '[') => in_quote = Some(ch),
628 (None, 'w' | 'W') => {
629 let end = i + 5;
630 if end <= tail.len()
631 && tail[i..end].eq_ignore_ascii_case("where")
632 && (i == 0 || !is_ident_byte(bytes[i - 1]))
633 && (end == tail.len() || !is_ident_byte(bytes[end]))
634 {
635 let clause = tail[end..].trim().trim_end_matches(';').trim();
636 if !clause.is_empty() {
637 parsed.where_clause = Some(clause.to_string());
638 }
639 break;
640 }
641 }
642 _ => {}
643 }
644 }
645
646 parsed
647}
648
649const fn quote_closer(open: char) -> char {
650 match open {
651 '[' => ']',
652 other => other,
653 }
654}
655
656const fn is_ident_byte(b: u8) -> bool {
657 b.is_ascii_alphanumeric() || b == b'_'
658}
659
660#[must_use]
666pub fn process_indexes<S: std::hash::BuildHasher>(
667 raw_indexes: &[RawIndexInfo],
668 index_columns: &[RawIndexColumn],
669 _table_sql_map: &std::collections::HashMap<String, String, S>,
670) -> Vec<Index> {
671 let empty: HashMap<String, String> = HashMap::new();
672 process_indexes_with_sql(raw_indexes, index_columns, &empty)
673}
674
675#[must_use]
681pub fn process_indexes_with_sql<S: std::hash::BuildHasher>(
682 raw_indexes: &[RawIndexInfo],
683 index_columns: &[RawIndexColumn],
684 index_sql_map: &std::collections::HashMap<String, String, S>,
685) -> Vec<Index> {
686 raw_indexes
687 .iter()
688 .filter(|idx| idx.origin == "c") .map(|idx| {
690 let parsed = index_sql_map.get(&idx.name).map(|sql| parse_index_sql(sql));
691
692 let mut key_columns: Vec<&RawIndexColumn> = index_columns
693 .iter()
694 .filter(|c| c.index_name == idx.name && c.key)
695 .collect();
696 key_columns.sort_by_key(|c| c.seqno);
697
698 let has_expression = key_columns.iter().any(|c| c.name.is_none());
699 let columns: Vec<IndexColumn> = match &parsed {
700 Some(parsed)
703 if (has_expression || key_columns.is_empty()) && !parsed.columns.is_empty() =>
704 {
705 parsed.columns.clone()
706 }
707 _ => key_columns
708 .iter()
709 .filter_map(|c| {
710 c.name.clone().map(|name| IndexColumn {
711 value: name.into(),
712 is_expression: false,
713 })
714 })
715 .collect(),
716 };
717
718 let where_clause = if idx.partial {
719 parsed
720 .as_ref()
721 .and_then(|p| p.where_clause.clone())
722 .map(std::convert::Into::into)
723 } else {
724 None
725 };
726
727 Index {
728 table: idx.table.clone().into(),
729 name: idx.name.clone().into(),
730 columns,
731 is_unique: idx.unique,
732 where_clause,
733 origin: IndexOrigin::Manual,
734 }
735 })
736 .collect()
737}
738
739#[must_use]
745pub fn process_unique_constraints_from_indexes(
746 raw_indexes: &[RawIndexInfo],
747 index_columns: &[RawIndexColumn],
748) -> Vec<UniqueConstraint> {
749 use std::borrow::Cow;
750
751 raw_indexes
752 .iter()
753 .filter(|idx| idx.origin == "u")
754 .filter_map(|idx| {
755 let mut cols: Vec<(i32, Cow<'static, str>)> = index_columns
756 .iter()
757 .filter(|c| c.index_name == idx.name && c.key)
758 .filter_map(|c| {
759 c.name
760 .as_ref()
761 .map(|name| (c.seqno, Cow::Owned(name.clone())))
762 })
763 .collect();
764
765 cols.sort_by_key(|(seq, _)| *seq);
766 let columns: Vec<Cow<'static, str>> = cols.into_iter().map(|(_, c)| c).collect();
767 if columns.is_empty() {
768 return None;
769 }
770
771 let columns_refs: Vec<&str> = columns.iter().map(std::convert::AsRef::as_ref).collect();
772 let name = super::ddl::name_for_unique(&idx.table, &columns_refs);
773
774 Some(UniqueConstraint {
775 table: Cow::Owned(idx.table.clone()),
776 name: Cow::Owned(name),
777 name_explicit: false,
778 columns: Cow::Owned(columns),
779 })
780 })
781 .collect()
782}
783
784#[must_use]
786pub fn process_foreign_keys(raw_fks: &[RawForeignKey]) -> Vec<ForeignKey> {
787 use std::borrow::Cow;
788
789 let mut grouped: BTreeMap<(String, i32), Vec<&RawForeignKey>> = BTreeMap::new();
792
793 for fk in raw_fks {
794 grouped
795 .entry((fk.table.clone(), fk.id))
796 .or_default()
797 .push(fk);
798 }
799
800 grouped
801 .into_iter()
802 .filter_map(|((table, _id), fks)| {
803 let mut fks = fks;
804 fks.sort_by_key(|f| f.seq);
805
806 let first = fks.first()?;
809
810 let columns: Vec<&str> = fks.iter().map(|f| f.from_column.as_str()).collect();
811 let columns_to: Vec<&str> = fks.iter().map(|f| f.to_column.as_str()).collect();
812
813 let name = super::ddl::name_for_fk(&table, &columns, &first.to_table, &columns_to);
814
815 let columns_cow: Vec<Cow<'static, str>> = fks
817 .iter()
818 .map(|f| Cow::Owned(f.from_column.clone()))
819 .collect();
820 let columns_to_cow: Vec<Cow<'static, str>> = fks
821 .iter()
822 .map(|f| Cow::Owned(f.to_column.clone()))
823 .collect();
824
825 Some(ForeignKey {
826 table: table.into(),
827 name: name.into(),
828 name_explicit: false,
829 columns: Cow::Owned(columns_cow),
830 table_to: first.to_table.clone().into(),
831 columns_to: Cow::Owned(columns_to_cow),
832 on_update: Some(first.on_update.clone().into()),
833 on_delete: Some(first.on_delete.clone().into()),
834 })
835 })
836 .collect()
837}
838
839pub fn create_primary_key(table: &str, pk_columns: Vec<String>) -> PrimaryKey {
844 use std::borrow::Cow;
845
846 let name = super::ddl::name_for_pk(table);
847 let columns_cow: Vec<Cow<'static, str>> = pk_columns.into_iter().map(Cow::Owned).collect();
848
849 PrimaryKey {
850 table: table.to_string().into(),
851 name: name.into(),
852 name_explicit: false,
853 columns: Cow::Owned(columns_cow),
854 }
855}
856
857pub fn create_unique_constraint(
859 table: &str,
860 name: &str,
861 columns: Vec<String>,
862 name_explicit: bool,
863) -> UniqueConstraint {
864 use std::borrow::Cow;
865
866 let columns_cow: Vec<Cow<'static, str>> = columns.into_iter().map(Cow::Owned).collect();
867
868 UniqueConstraint {
869 table: Cow::Owned(table.to_string()),
870 name: Cow::Owned(name.to_string()),
871 name_explicit,
872 columns: Cow::Owned(columns_cow),
873 }
874}
875
876#[must_use]
878pub fn process_unique_constraints_from_parsed(
879 table: &str,
880 parsed_uniques: &[super::ddl::ParsedUnique],
881) -> Vec<UniqueConstraint> {
882 use std::borrow::Cow;
883
884 parsed_uniques
885 .iter()
886 .map(|parsed| {
887 let columns_refs: Vec<&str> = parsed
888 .columns
889 .iter()
890 .map(std::string::String::as_str)
891 .collect();
892 let (name, name_explicit) = parsed.name.as_ref().map_or_else(
893 || (super::ddl::name_for_unique(table, &columns_refs), false),
894 |n| (n.clone(), true),
895 );
896 let columns_cow: Vec<Cow<'static, str>> = parsed
897 .columns
898 .iter()
899 .map(|c| Cow::Owned(c.clone()))
900 .collect();
901
902 UniqueConstraint {
903 table: table.to_string().into(),
904 name: name.into(),
905 name_explicit,
906 columns: Cow::Owned(columns_cow),
907 }
908 })
909 .collect()
910}
911
912pub mod queries {
914 pub const TABLES_QUERY: &str = r"
916 SELECT name, sql
917 FROM sqlite_master
918 WHERE type = 'table'
919 AND name != '__drizzle_migrations'
920 AND name NOT LIKE '\_cf\_%' ESCAPE '\'
921 AND name NOT LIKE '\_litestream\_%' ESCAPE '\'
922 AND name NOT LIKE 'libsql\_%' ESCAPE '\'
923 AND name NOT LIKE 'sqlite\_%' ESCAPE '\'
924 AND name NOT LIKE 'd1\_%' ESCAPE '\'
925 ORDER BY name COLLATE NOCASE
926 ";
927
928 pub const COLUMNS_QUERY: &str = r#"
930 SELECT
931 m.name as "table",
932 p.cid as "cid",
933 p.name as "name",
934 p.type as "columnType",
935 p."notnull" as "notNull",
936 p.dflt_value as "defaultValue",
937 p.pk as pk,
938 p.hidden as hidden,
939 m.sql
940 FROM sqlite_master AS m
941 JOIN pragma_table_xinfo(m.name) AS p
942 WHERE
943 m.type = 'table'
944 AND m.tbl_name != '__drizzle_migrations'
945 AND m.tbl_name NOT LIKE '\_cf\_%' ESCAPE '\'
946 AND m.tbl_name NOT LIKE '\_litestream\_%' ESCAPE '\'
947 AND m.tbl_name NOT LIKE 'libsql\_%' ESCAPE '\'
948 AND m.tbl_name NOT LIKE 'sqlite\_%' ESCAPE '\'
949 AND m.tbl_name NOT LIKE 'd1\_%' ESCAPE '\'
950 ORDER BY p.cid
951 "#;
952
953 pub const VIEWS_QUERY: &str = r"
955 SELECT name, sql
956 FROM sqlite_master
957 WHERE type = 'view'
958 AND name != '__drizzle_migrations'
959 AND name NOT LIKE '\_cf\_%' ESCAPE '\'
960 AND name NOT LIKE '\_litestream\_%' ESCAPE '\'
961 AND name NOT LIKE 'libsql\_%' ESCAPE '\'
962 AND name NOT LIKE 'sqlite\_%' ESCAPE '\'
963 AND name NOT LIKE 'd1\_%' ESCAPE '\'
964 ORDER BY name COLLATE NOCASE
965 ";
966
967 pub const VIEW_COLUMNS_QUERY: &str = r#"
969 SELECT
970 m.name as "table",
971 p.cid as "cid",
972 p.name as "name",
973 p.type as "columnType",
974 p."notnull" as "notNull",
975 p.dflt_value as "defaultValue",
976 p.pk as pk,
977 p.hidden as hidden,
978 m.sql
979 FROM sqlite_master AS m
980 JOIN pragma_table_xinfo(m.name) AS p
981 WHERE
982 m.type = 'view'
983 AND m.tbl_name != '__drizzle_migrations'
984 AND m.tbl_name NOT LIKE '\_cf\_%' ESCAPE '\'
985 AND m.tbl_name NOT LIKE '\_litestream\_%' ESCAPE '\'
986 AND m.tbl_name NOT LIKE 'libsql\_%' ESCAPE '\'
987 AND m.tbl_name NOT LIKE 'sqlite\_%' ESCAPE '\'
988 AND m.tbl_name NOT LIKE 'd1\_%' ESCAPE '\'
989 ORDER BY m.name, p.cid
990 "#;
991
992 pub const INDEXES_QUERY: &str = r#"
994 SELECT
995 m.name AS "table",
996 p.name,
997 p."unique",
998 p.origin,
999 p.partial
1000 FROM sqlite_master AS m
1001 JOIN pragma_index_list(m.name) AS p
1002 WHERE m.type = 'table'
1003 AND m.tbl_name != '__drizzle_migrations'
1004 AND m.tbl_name NOT LIKE '\_cf\_%' ESCAPE '\'
1005 AND m.tbl_name NOT LIKE '\_litestream\_%' ESCAPE '\'
1006 AND m.tbl_name NOT LIKE 'libsql\_%' ESCAPE '\'
1007 AND m.tbl_name NOT LIKE 'sqlite\_%' ESCAPE '\'
1008 AND m.tbl_name NOT LIKE 'd1\_%' ESCAPE '\'
1009 ORDER BY m.name COLLATE NOCASE, p.seq
1010 "#;
1011
1012 pub const INDEX_SQL_QUERY: &str = r"
1019 SELECT name, sql
1020 FROM sqlite_master
1021 WHERE type = 'index'
1022 AND sql IS NOT NULL
1023 AND tbl_name != '__drizzle_migrations'
1024 AND tbl_name NOT LIKE '\_cf\_%' ESCAPE '\'
1025 AND tbl_name NOT LIKE '\_litestream\_%' ESCAPE '\'
1026 AND tbl_name NOT LIKE 'libsql\_%' ESCAPE '\'
1027 AND tbl_name NOT LIKE 'sqlite\_%' ESCAPE '\'
1028 AND tbl_name NOT LIKE 'd1\_%' ESCAPE '\'
1029 ORDER BY name COLLATE NOCASE
1030 ";
1031
1032 pub const INDEX_COLUMNS_QUERY: &str = r#"
1034 SELECT
1035 indexes.name AS index_name,
1036 columns.seqno,
1037 columns.cid,
1038 columns.name,
1039 columns."desc",
1040 columns.coll,
1041 columns."key"
1042 FROM sqlite_master AS m
1043 JOIN pragma_index_list(m.name) AS indexes
1044 JOIN pragma_index_xinfo(indexes.name) AS columns
1045 WHERE m.type = 'table'
1046 AND m.tbl_name != '__drizzle_migrations'
1047 AND m.tbl_name NOT LIKE '\_cf\_%' ESCAPE '\'
1048 AND m.tbl_name NOT LIKE '\_litestream\_%' ESCAPE '\'
1049 AND m.tbl_name NOT LIKE 'libsql\_%' ESCAPE '\'
1050 AND m.tbl_name NOT LIKE 'sqlite\_%' ESCAPE '\'
1051 AND m.tbl_name NOT LIKE 'd1\_%' ESCAPE '\'
1052 ORDER BY m.name COLLATE NOCASE, indexes.seq, columns.seqno
1053 "#;
1054
1055 pub const FOREIGN_KEYS_QUERY: &str = r#"
1057 SELECT
1058 m.name AS "table",
1059 p.id,
1060 p.seq,
1061 p."table" AS to_table,
1062 p."from" AS from_column,
1063 p."to" AS to_column,
1064 p.on_update,
1065 p.on_delete,
1066 p."match"
1067 FROM sqlite_master AS m
1068 JOIN pragma_foreign_key_list(m.name) AS p
1069 WHERE m.type = 'table'
1070 AND m.tbl_name != '__drizzle_migrations'
1071 AND m.tbl_name NOT LIKE '\_cf\_%' ESCAPE '\'
1072 AND m.tbl_name NOT LIKE '\_litestream\_%' ESCAPE '\'
1073 AND m.tbl_name NOT LIKE 'libsql\_%' ESCAPE '\'
1074 AND m.tbl_name NOT LIKE 'sqlite\_%' ESCAPE '\'
1075 AND m.tbl_name NOT LIKE 'd1\_%' ESCAPE '\'
1076 ORDER BY m.name COLLATE NOCASE, p.id, p.seq
1077 "#;
1078}
1079
1080#[must_use]
1086pub fn parse_view_sql(sql: &str) -> Option<String> {
1087 let bytes = sql.as_bytes();
1088 let mut in_quote: Option<char> = None;
1089 let mut depth = 0i32;
1090
1091 for (i, ch) in sql.char_indices() {
1092 match (in_quote, ch) {
1093 (Some(q), _) if quote_closer(q) == ch => in_quote = None,
1094 (Some(_), _) => {}
1095 (None, '\'' | '"' | '`' | '[') => in_quote = Some(ch),
1096 (None, '(') => depth += 1,
1097 (None, ')') => depth -= 1,
1098 (None, 'a' | 'A') if depth == 0 => {
1099 let end = i + 2;
1100 if end <= sql.len()
1101 && sql[i..end].eq_ignore_ascii_case("as")
1102 && (i == 0 || !is_ident_byte(bytes[i - 1]))
1103 && (end == sql.len() || !is_ident_byte(bytes[end]))
1104 {
1105 let definition = sql[end..].trim().trim_end_matches(';').trim();
1106 if definition.is_empty() {
1107 return None;
1108 }
1109 return Some(definition.to_string());
1110 }
1111 }
1112 _ => {}
1113 }
1114 }
1115 None
1116}
1117
1118fn parse_generated_tail(rest: &str) -> Option<(String, GeneratedType)> {
1123 let upper_rest = rest.to_uppercase();
1124 let as_pos = upper_rest.find(" AS ")?;
1125 let after_as = &rest[as_pos + 4..];
1126 let expr_start_rel = after_as.find('(')?;
1127 let expr_start = as_pos + 4 + expr_start_rel;
1128
1129 let mut expr_depth = 0i32;
1130 let mut expr_end: Option<usize> = None;
1131 for (i, ch) in rest.char_indices().skip(expr_start) {
1132 match ch {
1133 '(' => expr_depth += 1,
1134 ')' => {
1135 expr_depth -= 1;
1136 if expr_depth == 0 {
1137 expr_end = Some(i);
1138 break;
1139 }
1140 }
1141 _ => {}
1142 }
1143 }
1144 let expr_end = expr_end?;
1145
1146 let expression = rest[expr_start + 1..expr_end].trim().to_string();
1147 let after_expr = rest[expr_end + 1..].to_uppercase();
1148 let gen_type = if after_expr.contains("STORED") {
1149 GeneratedType::Stored
1150 } else {
1151 GeneratedType::Virtual
1152 };
1153 Some((expression, gen_type))
1154}
1155
1156#[must_use]
1165pub fn parse_generated_columns_from_table_sql(
1166 table: &str,
1167 sql: &str,
1168) -> HashMap<String, ParsedGenerated> {
1169 let mut out: HashMap<String, ParsedGenerated> = HashMap::new();
1170
1171 let Some(body) = extract_table_body(sql) else {
1172 return out;
1173 };
1174
1175 for item in split_top_level_commas(body) {
1176 if item.is_empty() {
1177 continue;
1178 }
1179 let upper = item.to_uppercase();
1180 if !upper.contains("GENERATED") || is_table_level_constraint(&upper) {
1181 continue;
1182 }
1183
1184 let Some((col_name, rest)) = take_column_name(item) else {
1185 continue;
1186 };
1187 let Some((expression, gen_type)) = parse_generated_tail(rest) else {
1188 continue;
1189 };
1190
1191 out.insert(
1192 format!("{table}:{col_name}"),
1193 ParsedGenerated {
1194 expression,
1195 gen_type,
1196 },
1197 );
1198 }
1199
1200 out
1201}
1202
1203#[cfg(test)]
1204mod tests {
1205 use super::*;
1206
1207 #[test]
1208 fn test_system_table_filter() {
1209 assert!(!system_table_filter("sqlite_master"));
1210 assert!(!system_table_filter("__drizzle_migrations"));
1211 assert!(!system_table_filter("_cf_something"));
1212 assert!(system_table_filter("users"));
1213 assert!(system_table_filter("posts"));
1214 }
1215
1216 #[test]
1217 fn test_parse_view_sql() {
1218 let sql = "CREATE VIEW active_users AS SELECT * FROM users WHERE active = 1";
1219 let definition = parse_view_sql(sql);
1220 assert_eq!(
1221 definition,
1222 Some("SELECT * FROM users WHERE active = 1".to_string())
1223 );
1224 }
1225
1226 #[test]
1227 fn test_parse_autoincrement_columns_from_table_sql() {
1228 let sql = "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)";
1229 let cols = parse_autoincrement_columns_from_table_sql(sql);
1230 assert!(cols.contains("id"));
1231 assert!(!cols.contains("name"));
1232 }
1233
1234 #[test]
1235 fn test_parse_generated_columns_from_table_sql() {
1236 let sql = r#"
1237CREATE TABLE users (
1238 id INTEGER PRIMARY KEY,
1239 first TEXT,
1240 last TEXT,
1241 full TEXT GENERATED ALWAYS AS (first || ' ' || last) VIRTUAL,
1242 total INT GENERATED ALWAYS AS ((id + 1) * 2) STORED
1243);
1244"#;
1245 let map = parse_generated_columns_from_table_sql("users", sql);
1246 let full = map.get("users:full").expect("full generated");
1247 assert_eq!(full.gen_type, GeneratedType::Virtual);
1248 assert!(full.expression.contains("first"));
1249
1250 let total = map.get("users:total").expect("total generated");
1251 assert_eq!(total.gen_type, GeneratedType::Stored);
1252 assert!(total.expression.contains("id"));
1253 }
1254
1255 #[test]
1256 fn test_parse_table_options_ignores_body_content() {
1257 let sql = "CREATE TABLE t (strict TEXT, without_rowid INTEGER)";
1259 assert_eq!(parse_table_options(sql), (false, false));
1260
1261 let sql = "CREATE TABLE t (note TEXT DEFAULT 'WITHOUT ROWID STRICT')";
1263 assert_eq!(parse_table_options(sql), (false, false));
1264
1265 let sql = "CREATE TABLE t (id INTEGER) STRICT";
1266 assert_eq!(parse_table_options(sql), (true, false));
1267
1268 let sql = "CREATE TABLE t (id INTEGER) WITHOUT ROWID";
1269 assert_eq!(parse_table_options(sql), (false, true));
1270
1271 let sql = "CREATE TABLE t (id INTEGER) STRICT, WITHOUT ROWID;";
1272 assert_eq!(parse_table_options(sql), (true, true));
1273
1274 let sql = "CREATE TABLE t (id INTEGER) WITHOUT ROWID, STRICT;";
1275 assert_eq!(parse_table_options(sql), (true, true));
1276 }
1277
1278 #[test]
1279 fn test_parse_index_sql_recovers_where_and_expressions() {
1280 let parsed =
1281 parse_index_sql("CREATE INDEX idx_c_positive ON multi_indexed(col_c) WHERE col_c > 0");
1282 assert_eq!(parsed.columns.len(), 1);
1283 assert_eq!(parsed.columns[0].value, "col_c");
1284 assert!(!parsed.columns[0].is_expression);
1285 assert_eq!(parsed.where_clause.as_deref(), Some("col_c > 0"));
1286
1287 let parsed = parse_index_sql("CREATE UNIQUE INDEX i ON t(lower(email), name)");
1288 assert_eq!(parsed.columns.len(), 2);
1289 assert_eq!(parsed.columns[0].value, "lower(email)");
1290 assert!(parsed.columns[0].is_expression);
1291 assert_eq!(parsed.columns[1].value, "name");
1292 assert!(!parsed.columns[1].is_expression);
1293 assert!(parsed.where_clause.is_none());
1294
1295 let parsed =
1297 parse_index_sql("CREATE INDEX i ON t(`email` DESC, \"name\" COLLATE NOCASE ASC)");
1298 assert_eq!(parsed.columns.len(), 2);
1299 assert_eq!(parsed.columns[0].value, "email");
1300 assert!(!parsed.columns[0].is_expression);
1301 assert_eq!(parsed.columns[1].value, "name");
1302 assert!(!parsed.columns[1].is_expression);
1303
1304 let parsed = parse_index_sql("CREATE INDEX i ON t(coalesce(kind, 'WHERE x'))");
1306 assert!(parsed.where_clause.is_none());
1307 assert_eq!(parsed.columns.len(), 1);
1308 assert!(parsed.columns[0].is_expression);
1309 }
1310
1311 #[test]
1312 fn test_process_columns_keeps_generated_columns() {
1313 let table_sql = "CREATE TABLE g (id INTEGER PRIMARY KEY, v TEXT GENERATED ALWAYS AS (id + 1) VIRTUAL, s TEXT GENERATED ALWAYS AS (id + 2) STORED)";
1314 let raw = vec![
1315 RawColumnInfo {
1316 table: "g".to_string(),
1317 cid: 0,
1318 name: "id".to_string(),
1319 column_type: "INTEGER".to_string(),
1320 not_null: false,
1321 default_value: None,
1322 pk: 1,
1323 hidden: 0,
1324 sql: Some(table_sql.to_string()),
1325 },
1326 RawColumnInfo {
1327 table: "g".to_string(),
1328 cid: 1,
1329 name: "v".to_string(),
1330 column_type: "TEXT".to_string(),
1331 not_null: false,
1332 default_value: None,
1333 pk: 0,
1334 hidden: 2, sql: Some(table_sql.to_string()),
1336 },
1337 RawColumnInfo {
1338 table: "g".to_string(),
1339 cid: 2,
1340 name: "s".to_string(),
1341 column_type: "TEXT".to_string(),
1342 not_null: false,
1343 default_value: None,
1344 pk: 0,
1345 hidden: 3, sql: Some(table_sql.to_string()),
1347 },
1348 ];
1349
1350 let generated = parse_generated_columns_from_table_sql("g", table_sql);
1351 let pk_columns: HashSet<(String, String)> = HashSet::new();
1352 let (columns, _pks) = process_columns(&raw, &generated, &pk_columns);
1353
1354 assert_eq!(columns.len(), 3, "generated columns must be kept");
1355 let v = columns.iter().find(|c| c.name == "v").expect("v column");
1356 let v_generated = v.generated.as_ref().expect("v generated info");
1357 assert_eq!(v_generated.gen_type, GeneratedType::Virtual);
1358 assert_eq!(v_generated.expression, "id + 1");
1359 let s = columns.iter().find(|c| c.name == "s").expect("s column");
1360 let s_generated = s.generated.as_ref().expect("s generated info");
1361 assert_eq!(s_generated.gen_type, GeneratedType::Stored);
1362 assert_eq!(s_generated.expression, "id + 2");
1363 }
1364
1365 #[test]
1366 fn test_composite_pk_columns_ordered_by_pk_position() {
1367 let raw = vec![
1369 RawColumnInfo {
1370 table: "t".to_string(),
1371 cid: 0,
1372 name: "a".to_string(),
1373 column_type: "INTEGER".to_string(),
1374 not_null: true,
1375 default_value: None,
1376 pk: 2,
1377 hidden: 0,
1378 sql: None,
1379 },
1380 RawColumnInfo {
1381 table: "t".to_string(),
1382 cid: 1,
1383 name: "b".to_string(),
1384 column_type: "INTEGER".to_string(),
1385 not_null: true,
1386 default_value: None,
1387 pk: 1,
1388 hidden: 0,
1389 sql: None,
1390 },
1391 ];
1392 let generated = HashMap::new();
1393 let pk_columns: HashSet<(String, String)> = HashSet::new();
1394 let (_cols, pks) = process_columns(&raw, &generated, &pk_columns);
1395 assert_eq!(pks.len(), 1);
1396 let cols: Vec<&str> = pks[0].columns.iter().map(AsRef::as_ref).collect();
1397 assert_eq!(cols, vec!["b", "a"]);
1398 }
1399
1400 #[test]
1401 fn test_parse_view_sql_is_quote_aware() {
1402 let sql = r#"CREATE VIEW "my as view" AS SELECT * FROM users"#;
1403 assert_eq!(parse_view_sql(sql), Some("SELECT * FROM users".to_string()));
1404
1405 let sql = "CREATE VIEW v(a, b) AS SELECT 1, 2";
1407 assert_eq!(parse_view_sql(sql), Some("SELECT 1, 2".to_string()));
1408
1409 let sql = "CREATE VIEW basics AS SELECT 1";
1411 assert_eq!(parse_view_sql(sql), Some("SELECT 1".to_string()));
1412 }
1413
1414 #[test]
1415 fn set_based_metadata_queries_cover_all_tables_and_indexes() {
1416 let connection = rusqlite::Connection::open_in_memory().expect("open SQLite");
1417 connection
1418 .execute_batch(
1419 "PRAGMA foreign_keys = ON;
1420 CREATE TABLE parents(id INTEGER PRIMARY KEY);
1421 CREATE TABLE children(
1422 id INTEGER PRIMARY KEY,
1423 parent_id INTEGER NOT NULL,
1424 email TEXT UNIQUE,
1425 FOREIGN KEY(parent_id) REFERENCES parents(id)
1426 );
1427 CREATE INDEX children_parent_idx ON children(parent_id);",
1428 )
1429 .expect("create schema");
1430
1431 let index_count: i64 = connection
1432 .query_row(
1433 &format!("SELECT COUNT(*) FROM ({})", queries::INDEXES_QUERY),
1434 [],
1435 |row| row.get(0),
1436 )
1437 .expect("query all indexes");
1438 let indexed_column_count: i64 = connection
1439 .query_row(
1440 &format!("SELECT COUNT(*) FROM ({})", queries::INDEX_COLUMNS_QUERY),
1441 [],
1442 |row| row.get(0),
1443 )
1444 .expect("query all index columns");
1445 let foreign_key_count: i64 = connection
1446 .query_row(
1447 &format!("SELECT COUNT(*) FROM ({})", queries::FOREIGN_KEYS_QUERY),
1448 [],
1449 |row| row.get(0),
1450 )
1451 .expect("query all foreign keys");
1452
1453 assert!(index_count >= 2);
1454 assert!(indexed_column_count >= 2);
1455 assert_eq!(foreign_key_count, 1);
1456 }
1457}