1use crate::error::EFResult;
10use crate::metadata::{EntityTypeMeta, NavigationKind};
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone)]
15pub struct Migration {
16 pub id: String,
17 pub description: String,
18 pub up_sql: String,
19 pub down_sql: String,
20}
21
22#[derive(Debug, Clone)]
25pub struct ModelSnapshot {
26 pub migration_id: String,
27 pub entity_types: Vec<SnapshotEntityType>,
28}
29
30#[derive(Debug, Clone)]
32pub struct SnapshotEntityType {
33 pub type_name: String,
34 pub table_name: String,
35 pub columns: Vec<SnapshotColumn>,
36}
37
38#[derive(Debug, Clone, PartialEq, Default)]
40pub struct SnapshotColumn {
41 pub field_name: String,
42 pub column_name: String,
43 pub type_name: String,
44 pub is_primary_key: bool,
45 pub is_required: bool,
46 pub is_foreign_key: bool,
47 pub max_length: Option<usize>,
48 pub is_auto_increment: bool,
49 pub fk_referenced_table: Option<String>,
51 pub fk_referenced_column: Option<String>,
53 pub has_index: bool,
55 pub is_unique: bool,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum MigrationDialect {
62 Postgres,
63 MySql,
64 Sqlite,
65}
66
67impl MigrationDialect {
68 pub fn quote(&self, ident: &str) -> String {
70 match self {
71 MigrationDialect::Postgres | MigrationDialect::Sqlite => format!("\"{}\"", ident),
72 MigrationDialect::MySql => format!("`{}`", ident),
73 }
74 }
75
76 pub fn map_column_type(&self, col: &SnapshotColumn) -> String {
78 let tn = col.type_name.as_str();
82
83 if col.is_auto_increment {
85 if tn.ends_with("i32") {
86 return match self {
87 MigrationDialect::Postgres => "SERIAL".into(),
88 MigrationDialect::MySql => "INT AUTO_INCREMENT".into(),
89 MigrationDialect::Sqlite => "INTEGER".into(),
90 };
91 }
92 if tn.ends_with("i64") {
93 return match self {
94 MigrationDialect::Postgres => "BIGSERIAL".into(),
95 MigrationDialect::MySql => "BIGINT AUTO_INCREMENT".into(),
96 MigrationDialect::Sqlite => "INTEGER".into(),
97 };
98 }
99 }
100
101 let base: &str = if tn.ends_with("i16") {
102 "SMALLINT"
103 } else if tn.ends_with("i32") {
104 "INTEGER"
105 } else if tn.ends_with("i64") {
106 "BIGINT"
107 } else if tn.ends_with("f32") {
108 "REAL"
109 } else if tn.ends_with("f64") {
110 "DOUBLE PRECISION"
111 } else if tn.ends_with("bool") {
112 "BOOLEAN"
113 } else if tn.ends_with("String") {
114 return match col.max_length {
115 Some(n) => format!("VARCHAR({})", n),
116 None => "TEXT".into(),
117 };
118 } else if tn.ends_with("Vec<u8>") {
119 return match self {
120 MigrationDialect::Postgres => "BYTEA".into(),
121 MigrationDialect::MySql | MigrationDialect::Sqlite => "BLOB".into(),
122 };
123 } else if tn.contains("NaiveDateTime") {
124 return match self {
125 MigrationDialect::Postgres => "TIMESTAMP".into(),
126 MigrationDialect::MySql => "DATETIME".into(),
127 MigrationDialect::Sqlite => "TEXT".into(),
128 };
129 } else if tn.contains("NaiveDate") {
130 return match self {
131 MigrationDialect::Postgres => "DATE".into(),
132 MigrationDialect::MySql => "DATE".into(),
133 MigrationDialect::Sqlite => "TEXT".into(),
134 };
135 } else if tn.contains("DateTime") {
136 return match self {
138 MigrationDialect::Postgres => "TIMESTAMPTZ".into(),
139 MigrationDialect::MySql => "DATETIME".into(),
140 MigrationDialect::Sqlite => "TEXT".into(),
141 };
142 } else if tn.contains("Uuid") {
143 return match self {
144 MigrationDialect::Postgres => "UUID".into(),
145 MigrationDialect::MySql => "CHAR(36)".into(),
146 MigrationDialect::Sqlite => "TEXT".into(),
147 };
148 } else if tn.contains("Decimal") {
149 return match self {
150 MigrationDialect::Postgres => "NUMERIC".into(),
151 MigrationDialect::MySql => "DECIMAL(38,18)".into(),
152 MigrationDialect::Sqlite => "TEXT".into(),
153 };
154 } else {
155 "TEXT"
156 };
157 base.to_string()
158 }
159}
160
161#[derive(Debug, Clone)]
163pub(crate) enum SchemaChange {
164 CreateTable {
165 table: String,
166 columns: Vec<SnapshotColumn>,
167 },
168 DropTable {
169 table: String,
170 },
171 AddColumn {
172 table: String,
173 column: SnapshotColumn,
174 },
175 DropColumn {
176 table: String,
177 column_name: String,
178 },
179 AlterColumn {
180 table: String,
181 column_name: String,
182 old: SnapshotColumn,
183 new: SnapshotColumn,
184 },
185 AddForeignKey {
186 table: String,
187 column: String,
188 referenced_table: String,
189 referenced_column: String,
190 },
191 DropForeignKey {
192 table: String,
193 column: String,
194 referenced_table: String,
195 },
196 CreateIndex {
197 table: String,
198 column: String,
199 is_unique: bool,
200 },
201 DropIndex {
202 table: String,
203 column: String,
204 is_unique: bool,
205 },
206}
207
208pub struct MigrationEngine {
211 dialect: MigrationDialect,
212}
213
214impl MigrationEngine {
215 pub fn new(dialect: MigrationDialect) -> Self {
216 Self { dialect }
217 }
218
219 pub fn generate(
221 &self,
222 name: &str,
223 current: &[EntityTypeMeta],
224 previous_snapshot: &Option<ModelSnapshot>,
225 ) -> EFResult<Migration> {
226 let current_snapshot = self.create_snapshot("__current__", current);
227
228 let changes = match previous_snapshot {
229 Some(prev) => self.diff(prev, ¤t_snapshot),
230 None => self.initial_create_with_fks(¤t_snapshot),
231 };
232
233 let up_sql = self.generate_up_sql(&changes);
234 let down_sql = self.generate_down_sql(&changes);
235
236 Ok(Migration {
237 id: name.to_string(),
238 description: name.to_string(),
239 up_sql,
240 down_sql,
241 })
242 }
243
244 pub fn create_snapshot(
246 &self,
247 migration_id: &str,
248 entity_types: &[EntityTypeMeta],
249 ) -> ModelSnapshot {
250 let types = entity_types
251 .iter()
252 .map(|et| SnapshotEntityType {
253 type_name: et.type_name.to_string(),
254 table_name: et.table_name.to_string(),
255 columns: et
256 .properties
257 .iter()
258 .filter(|p| !p.is_not_mapped)
259 .map(|p| {
260 let (fk_table, fk_col) =
261 fk_reference_for_property(et, p.field_name.as_ref());
262 SnapshotColumn {
263 field_name: p.field_name.to_string(),
264 column_name: p.column_name.to_string(),
265 type_name: p.type_name.to_string(),
266 is_primary_key: p.is_primary_key,
267 is_required: p.is_required,
268 is_foreign_key: p.is_foreign_key,
269 max_length: p.max_length,
270 is_auto_increment: p.is_auto_increment,
271 fk_referenced_table: fk_table,
272 fk_referenced_column: fk_col,
273 has_index: p.has_index,
274 is_unique: p.is_unique,
275 }
276 })
277 .collect(),
278 })
279 .collect();
280
281 ModelSnapshot {
282 migration_id: migration_id.to_string(),
283 entity_types: types,
284 }
285 }
286
287 fn initial_create(&self, current: &ModelSnapshot) -> Vec<SchemaChange> {
292 current
293 .entity_types
294 .iter()
295 .map(|et| SchemaChange::CreateTable {
296 table: et.table_name.clone(),
297 columns: et.columns.clone(),
298 })
299 .collect()
300 }
301
302 fn diff(&self, old: &ModelSnapshot, new: &ModelSnapshot) -> Vec<SchemaChange> {
303 let mut changes = Vec::new();
304
305 let old_tables: HashMap<&str, &SnapshotEntityType> = old
307 .entity_types
308 .iter()
309 .map(|e| (e.table_name.as_str(), e))
310 .collect();
311 let new_tables: HashMap<&str, &SnapshotEntityType> = new
312 .entity_types
313 .iter()
314 .map(|e| (e.table_name.as_str(), e))
315 .collect();
316
317 let old_names: HashSet<&str> = old_tables.keys().copied().collect();
318 let new_names: HashSet<&str> = new_tables.keys().copied().collect();
319
320 for name in old_names.difference(&new_names) {
322 changes.push(SchemaChange::DropTable {
323 table: name.to_string(),
324 });
325 }
326
327 for name in new_names.difference(&old_names) {
329 let et = new_tables[name];
330 changes.push(SchemaChange::CreateTable {
331 table: et.table_name.clone(),
332 columns: et.columns.clone(),
333 });
334 Self::append_create_table_fks(&mut changes, &et.table_name, &et.columns);
335 Self::append_create_table_indexes(&mut changes, &et.table_name, &et.columns);
336 }
337
338 for name in old_names.intersection(&new_names) {
340 let old_et = old_tables[name];
341 let new_et = new_tables[name];
342 let table = &old_et.table_name;
343
344 let old_cols: HashMap<&str, &SnapshotColumn> = old_et
345 .columns
346 .iter()
347 .map(|c| (c.column_name.as_str(), c))
348 .collect();
349 let new_cols: HashMap<&str, &SnapshotColumn> = new_et
350 .columns
351 .iter()
352 .map(|c| (c.column_name.as_str(), c))
353 .collect();
354
355 let old_col_names: HashSet<&str> = old_cols.keys().copied().collect();
356 let new_col_names: HashSet<&str> = new_cols.keys().copied().collect();
357
358 for col_name in new_col_names.difference(&old_col_names) {
360 let col = new_cols[col_name];
361 changes.push(SchemaChange::AddColumn {
362 table: table.clone(),
363 column: (*col).clone(),
364 });
365 changes.extend(diff_indexes(table, &SnapshotColumn::default(), col));
366 }
367
368 changes.extend(diff_foreign_keys(table, old_et, new_et));
370
371 for col_name in old_col_names.difference(&new_col_names) {
373 changes.push(SchemaChange::DropColumn {
374 table: table.clone(),
375 column_name: col_name.to_string(),
376 });
377 }
378
379 for col_name in old_col_names.intersection(&new_col_names) {
381 let old_col = old_cols[col_name];
382 let new_col = new_cols[col_name];
383
384 changes.extend(diff_indexes(table, old_col, new_col));
386
387 if !columns_structurally_equal(old_col, new_col) {
389 changes.push(SchemaChange::AlterColumn {
390 table: table.clone(),
391 column_name: col_name.to_string(),
392 old: (*old_col).clone(),
393 new: (*new_col).clone(),
394 });
395 }
396 }
397 }
398
399 changes
400 }
401
402 fn append_create_table_fks(
403 changes: &mut Vec<SchemaChange>,
404 table: &str,
405 columns: &[SnapshotColumn],
406 ) {
407 for col in columns {
408 if let Some((ref_table, ref_col)) = fk_target(col) {
409 changes.push(SchemaChange::AddForeignKey {
410 table: table.to_string(),
411 column: col.column_name.clone(),
412 referenced_table: ref_table,
413 referenced_column: ref_col,
414 });
415 }
416 }
417 }
418
419 fn append_create_table_indexes(
421 changes: &mut Vec<SchemaChange>,
422 table: &str,
423 columns: &[SnapshotColumn],
424 ) {
425 for col in columns {
426 if col.is_unique {
427 changes.push(SchemaChange::CreateIndex {
428 table: table.to_string(),
429 column: col.column_name.clone(),
430 is_unique: true,
431 });
432 } else if col.has_index {
433 changes.push(SchemaChange::CreateIndex {
434 table: table.to_string(),
435 column: col.column_name.clone(),
436 is_unique: false,
437 });
438 }
439 }
440 }
441}
442
443fn fk_target(col: &SnapshotColumn) -> Option<(String, String)> {
444 if !col.is_foreign_key {
445 return None;
446 }
447 Some((
448 col.fk_referenced_table.clone()?,
449 col.fk_referenced_column.clone()?,
450 ))
451}
452
453fn index_name(table: &str, column: &str) -> String {
455 format!("ix_{}_{}", table, column)
456}
457
458fn fk_reference_for_property(
459 meta: &EntityTypeMeta,
460 field_name: &str,
461) -> (Option<String>, Option<String>) {
462 for nav in &meta.navigations {
463 if nav.kind != NavigationKind::BelongsTo {
464 continue;
465 }
466 let matches = nav
467 .foreign_key_field
468 .as_ref()
469 .is_some_and(|fk| fk.as_ref() == field_name);
470 if matches {
471 return (
472 nav.related_table.as_ref().map(|s| s.to_string()),
473 nav.referenced_key_column.as_ref().map(|s| s.to_string()),
474 );
475 }
476 }
477 (None, None)
478}
479
480fn diff_foreign_keys(
481 table: &str,
482 old_et: &SnapshotEntityType,
483 new_et: &SnapshotEntityType,
484) -> Vec<SchemaChange> {
485 let mut changes = Vec::new();
486 let old_fks: HashMap<&str, (&SnapshotColumn, String, String)> = old_et
487 .columns
488 .iter()
489 .filter_map(|c| {
490 let (rt, rc) = fk_target(c)?;
491 Some((c.column_name.as_str(), (c, rt, rc)))
492 })
493 .collect();
494 let new_fks: HashMap<&str, (&SnapshotColumn, String, String)> = new_et
495 .columns
496 .iter()
497 .filter_map(|c| {
498 let (rt, rc) = fk_target(c)?;
499 Some((c.column_name.as_str(), (c, rt, rc)))
500 })
501 .collect();
502
503 let old_names: HashSet<&str> = old_fks.keys().copied().collect();
504 let new_names: HashSet<&str> = new_fks.keys().copied().collect();
505
506 for col in new_names.difference(&old_names) {
507 let (_, rt, rc) = &new_fks[col];
508 changes.push(SchemaChange::AddForeignKey {
509 table: table.to_string(),
510 column: (*col).to_string(),
511 referenced_table: rt.clone(),
512 referenced_column: rc.clone(),
513 });
514 }
515
516 for col in old_names.difference(&new_names) {
517 let (_, rt, _) = &old_fks[col];
518 changes.push(SchemaChange::DropForeignKey {
519 table: table.to_string(),
520 column: (*col).to_string(),
521 referenced_table: rt.clone(),
522 });
523 }
524
525 for col in old_names.intersection(&new_names) {
526 let (_, old_rt, old_rc) = &old_fks[col];
527 let (_, new_rt, new_rc) = &new_fks[col];
528 if old_rt != new_rt || old_rc != new_rc {
529 changes.push(SchemaChange::DropForeignKey {
530 table: table.to_string(),
531 column: (*col).to_string(),
532 referenced_table: old_rt.clone(),
533 });
534 changes.push(SchemaChange::AddForeignKey {
535 table: table.to_string(),
536 column: (*col).to_string(),
537 referenced_table: new_rt.clone(),
538 referenced_column: new_rc.clone(),
539 });
540 }
541 }
542
543 changes
544}
545
546fn columns_structurally_equal(a: &SnapshotColumn, b: &SnapshotColumn) -> bool {
551 a.field_name == b.field_name
552 && a.column_name == b.column_name
553 && a.type_name == b.type_name
554 && a.is_primary_key == b.is_primary_key
555 && a.is_required == b.is_required
556 && a.is_foreign_key == b.is_foreign_key
557 && a.max_length == b.max_length
558 && a.is_auto_increment == b.is_auto_increment
559 && a.fk_referenced_table == b.fk_referenced_table
560 && a.fk_referenced_column == b.fk_referenced_column
561}
562
563fn diff_indexes(table: &str, old: &SnapshotColumn, new: &SnapshotColumn) -> Vec<SchemaChange> {
566 let old_kind = index_kind(old);
567 let new_kind = index_kind(new);
568 if old_kind == new_kind {
569 return Vec::new();
570 }
571 let mut changes = Vec::new();
572 if old_kind != IndexKind::None {
573 changes.push(SchemaChange::DropIndex {
574 table: table.to_string(),
575 column: new.column_name.clone(),
576 is_unique: old_kind == IndexKind::Unique,
577 });
578 }
579 if new_kind != IndexKind::None {
580 changes.push(SchemaChange::CreateIndex {
581 table: table.to_string(),
582 column: new.column_name.clone(),
583 is_unique: new_kind == IndexKind::Unique,
584 });
585 }
586 changes
587}
588
589#[derive(Clone, Copy, PartialEq, Eq)]
590enum IndexKind {
591 None,
592 NonUnique,
593 Unique,
594}
595
596fn index_kind(col: &SnapshotColumn) -> IndexKind {
597 if col.is_unique {
598 IndexKind::Unique
599 } else if col.has_index {
600 IndexKind::NonUnique
601 } else {
602 IndexKind::None
603 }
604}
605
606impl MigrationEngine {
607 fn initial_create_with_fks(&self, current: &ModelSnapshot) -> Vec<SchemaChange> {
608 let mut changes = self.initial_create(current);
609 for et in ¤t.entity_types {
610 Self::append_create_table_fks(&mut changes, &et.table_name, &et.columns);
611 Self::append_create_table_indexes(&mut changes, &et.table_name, &et.columns);
612 }
613 changes
614 }
615
616 pub fn foreign_key_name(table: &str, column: &str, referenced_table: &str) -> String {
622 format!("fk_{}_{}_{}", table, column, referenced_table)
623 }
624
625 pub fn index_name(table: &str, column: &str) -> String {
627 format!("ix_{}_{}", table, column)
628 }
629
630 pub fn generate_alter_column_sql(
631 &self,
632 table: &str,
633 column_name: &str,
634 new: &SnapshotColumn,
635 ) -> String {
636 let q = |s: &str| self.dialect.quote(s);
637 let col_type = self.dialect.map_column_type(new);
638 match self.dialect {
639 MigrationDialect::Postgres => {
640 let nullable = if new.is_required {
641 "SET NOT NULL"
642 } else {
643 "DROP NOT NULL"
644 };
645 format!(
646 "ALTER TABLE {} ALTER COLUMN {} TYPE {};\nALTER TABLE {} ALTER COLUMN {} {};\n",
647 q(table),
648 q(column_name),
649 col_type,
650 q(table),
651 q(column_name),
652 nullable
653 )
654 }
655 MigrationDialect::MySql => {
656 let nullable = if new.is_required { "NOT NULL" } else { "NULL" };
657 format!(
658 "ALTER TABLE {} MODIFY COLUMN {} {} {};\n",
659 q(table),
660 q(column_name),
661 col_type,
662 nullable
663 )
664 }
665 MigrationDialect::Sqlite => format!(
666 "-- SQLite does not support ALTER COLUMN type changes; \
667 rebuild table manually for {}.{}\n",
668 q(table),
669 q(column_name)
670 ),
671 }
672 }
673
674 fn generate_up_sql(&self, changes: &[SchemaChange]) -> String {
675 self.generate_up_sql_inner(changes, true)
676 }
677
678 fn generate_ddl_sql(&self, changes: &[SchemaChange]) -> String {
679 self.generate_up_sql_inner(changes, false)
680 }
681
682 fn generate_up_sql_inner(&self, changes: &[SchemaChange], record_history: bool) -> String {
683 let mut sql = String::from("-- Up Migration (auto-generated by rust-ef)\n\n");
684 let q = |s: &str| self.dialect.quote(s);
685 let create_kw = if record_history {
686 "CREATE TABLE"
687 } else {
688 "CREATE TABLE IF NOT EXISTS"
689 };
690
691 for change in changes {
692 match change {
693 SchemaChange::CreateTable { table, columns } => {
694 sql.push_str(&format!("{} {} (\n", create_kw, q(table)));
695
696 let pk_columns: Vec<&str> = columns
698 .iter()
699 .filter(|c| c.is_primary_key)
700 .map(|c| c.column_name.as_str())
701 .collect();
702
703 let col_defs: Vec<String> = columns
704 .iter()
705 .map(|c| {
706 let nullable = if c.is_required { "NOT NULL" } else { "NULL" };
707 let col_type = self.dialect.map_column_type(c);
708 [q(&c.column_name), col_type, nullable.to_string()]
710 .into_iter()
711 .filter(|s| !s.is_empty())
712 .collect::<Vec<_>>()
713 .join(" ")
714 })
715 .collect();
716 sql.push_str(&format!(" {}\n", col_defs.join(",\n ")));
717
718 if pk_columns.len() == 1 {
720 sql.push_str(&format!(" ,PRIMARY KEY ({})\n", q(pk_columns[0])));
721 } else if pk_columns.len() > 1 {
722 let pk_list: Vec<String> = pk_columns.iter().map(|c| q(c)).collect();
723 sql.push_str(&format!(" ,PRIMARY KEY ({})\n", pk_list.join(", ")));
724 }
725
726 sql.push_str(");\n\n");
727 }
728 SchemaChange::DropTable { table } => {
729 sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
730 }
731 SchemaChange::AddColumn { table, column } => {
732 let col_type = self.dialect.map_column_type(column);
733 let nullable = if column.is_required {
734 "NOT NULL"
735 } else {
736 "NULL"
737 };
738 sql.push_str(&format!(
739 "ALTER TABLE {} ADD COLUMN {} {} {};\n",
740 q(table),
741 q(&column.column_name),
742 col_type,
743 nullable
744 ));
745 }
746 SchemaChange::DropColumn { table, column_name } => {
747 sql.push_str(&format!(
748 "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
749 q(table),
750 q(column_name)
751 ));
752 }
753 SchemaChange::AlterColumn {
754 table,
755 column_name,
756 old: _,
757 new,
758 } => {
759 sql.push_str(&self.generate_alter_column_sql(table, column_name, new));
760 }
761 SchemaChange::AddForeignKey {
762 table,
763 column,
764 referenced_table,
765 referenced_column,
766 } => {
767 let fk_name = Self::foreign_key_name(table, column, referenced_table);
768 sql.push_str(&format!(
769 "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({});\n",
770 q(table),
771 q(&fk_name),
772 q(column),
773 q(referenced_table),
774 q(referenced_column)
775 ));
776 }
777 SchemaChange::DropForeignKey {
778 table,
779 column,
780 referenced_table,
781 } => {
782 let fk_name = Self::foreign_key_name(table, column, referenced_table);
783 match self.dialect {
784 MigrationDialect::MySql => sql.push_str(&format!(
785 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
786 q(table),
787 q(&fk_name)
788 )),
789 _ => sql.push_str(&format!(
790 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
791 q(table),
792 q(&fk_name)
793 )),
794 }
795 }
796 SchemaChange::CreateIndex {
797 table,
798 column,
799 is_unique,
800 } => {
801 let unique_kw = if *is_unique { "UNIQUE " } else { "" };
802 let idx_name = index_name(table, column);
803 sql.push_str(&format!(
804 "CREATE {unique_kw}INDEX {} ON {} ({});\n",
805 q(&idx_name),
806 q(table),
807 q(column)
808 ));
809 }
810 SchemaChange::DropIndex {
811 table,
812 column,
813 is_unique: _,
814 } => {
815 let idx_name = index_name(table, column);
816 match self.dialect {
817 MigrationDialect::MySql => {
818 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
819 }
820 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
821 }
822 }
823 }
824 }
825
826 if record_history {
827 sql.push_str(&format!(
828 "INSERT INTO {}(migration_id, product_version) VALUES ('{{migration_id}}', '{}');\n",
829 q(MIGRATION_HISTORY_TABLE),
830 PRODUCT_VERSION
831 ));
832 }
833
834 sql
835 }
836
837 fn generate_down_sql(&self, changes: &[SchemaChange]) -> String {
838 let mut sql = String::from("-- Down Migration (auto-generated by rust-ef)\n\n");
839 let q = |s: &str| self.dialect.quote(s);
840
841 for change in changes.iter().rev() {
843 match change {
844 SchemaChange::CreateTable { table, .. } => {
845 sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
846 }
847 SchemaChange::DropTable { table } => {
848 sql.push_str(&format!(
850 "-- WARNING: Cannot restore table {} (original schema unknown)\n",
851 q(table)
852 ));
853 }
854 SchemaChange::AddColumn { table, column } => {
855 sql.push_str(&format!(
856 "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
857 q(table),
858 q(&column.column_name)
859 ));
860 }
861 SchemaChange::DropColumn { table, column_name } => {
862 sql.push_str(&format!(
864 "-- WARNING: Cannot restore column {} on {} (original type unknown)\n",
865 q(column_name),
866 q(table)
867 ));
868 }
869 SchemaChange::AlterColumn {
870 table,
871 column_name,
872 old,
873 new: _,
874 } => {
875 sql.push_str(&self.generate_alter_column_sql(table, column_name, old));
876 }
877 SchemaChange::AddForeignKey {
878 table,
879 column,
880 referenced_table,
881 ..
882 } => {
883 let fk_name = Self::foreign_key_name(table, column, referenced_table);
884 match self.dialect {
885 MigrationDialect::MySql => sql.push_str(&format!(
886 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
887 q(table),
888 q(&fk_name)
889 )),
890 _ => sql.push_str(&format!(
891 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
892 q(table),
893 q(&fk_name)
894 )),
895 }
896 }
897 SchemaChange::DropForeignKey {
898 table,
899 column,
900 referenced_table,
901 ..
902 } => {
903 sql.push_str(&format!(
904 "-- WARNING: Cannot restore foreign key constraint {} on {}.{}\n",
905 q(&Self::foreign_key_name(table, column, referenced_table)),
906 q(table),
907 q(column)
908 ));
909 }
910 SchemaChange::CreateIndex {
911 table,
912 column,
913 is_unique: _,
914 } => {
915 let idx_name = index_name(table, column);
916 match self.dialect {
917 MigrationDialect::MySql => {
918 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
919 }
920 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
921 }
922 }
923 SchemaChange::DropIndex {
924 table,
925 column,
926 is_unique,
927 } => {
928 let unique_kw = if *is_unique { "UNIQUE " } else { "" };
929 let idx_name = index_name(table, column);
930 sql.push_str(&format!(
931 "CREATE {unique_kw}INDEX {} ON {} ({});\n",
932 q(&idx_name),
933 q(table),
934 q(column)
935 ));
936 }
937 }
938 }
939
940 sql.push_str(&format!(
941 "DELETE FROM {} WHERE migration_id = '{{migration_id}}';\n",
942 q(MIGRATION_HISTORY_TABLE)
943 ));
944
945 sql
946 }
947
948 pub async fn ensure_history_table(
950 &self,
951 provider: &dyn crate::provider::IDatabaseProvider,
952 ) -> EFResult<()> {
953 let sql = create_migration_history_table_sql(self.dialect);
954 provider.execute_migration_command(&sql).await
955 }
956
957 pub async fn apply(
959 &self,
960 provider: &dyn crate::provider::IDatabaseProvider,
961 migration: &Migration,
962 ) -> EFResult<()> {
963 if self.is_applied(provider, &migration.id).await? {
964 return Ok(());
965 }
966 self.ensure_history_table(provider).await?;
967 let sql = migration.up_sql.replace("{migration_id}", &migration.id);
968 for statement in split_sql_statements(&sql) {
969 if !statement.is_empty() {
970 provider.execute_migration_command(&statement).await?;
971 }
972 }
973 Ok(())
974 }
975
976 pub async fn revert(
978 &self,
979 provider: &dyn crate::provider::IDatabaseProvider,
980 migration: &Migration,
981 ) -> EFResult<()> {
982 if !self.is_applied(provider, &migration.id).await? {
983 return Err(crate::error::EFError::migration(format!(
984 "migration '{}' is not applied",
985 migration.id
986 )));
987 }
988 let sql = migration.down_sql.replace("{migration_id}", &migration.id);
989 for statement in split_sql_statements(&sql) {
990 if !statement.is_empty() {
991 provider.execute_migration_command(&statement).await?;
992 }
993 }
994 Ok(())
995 }
996
997 pub async fn get_applied_migrations(
999 &self,
1000 provider: &dyn crate::provider::IDatabaseProvider,
1001 ) -> EFResult<Vec<MigrationHistoryEntry>> {
1002 self.ensure_history_table(provider).await?;
1003 let q = |s: &str| self.dialect.quote(s);
1004 let table = q(MIGRATION_HISTORY_TABLE);
1005 let sql = format!(
1006 "SELECT {}, {} FROM {} ORDER BY {}",
1007 q("migration_id"),
1008 q("product_version"),
1009 table,
1010 q("migration_id")
1011 );
1012 let mut conn = provider.get_connection().await?;
1013 let rows = conn.query(&sql, &[]).await?;
1014 Ok(rows
1015 .into_iter()
1016 .map(|row| MigrationHistoryEntry {
1017 migration_id: row
1018 .first()
1019 .and_then(|v| String::try_from(v.clone()).ok())
1020 .unwrap_or_default(),
1021 product_version: row
1022 .get(1)
1023 .and_then(|v| String::try_from(v.clone()).ok())
1024 .unwrap_or_default(),
1025 })
1026 .collect())
1027 }
1028
1029 pub async fn is_applied(
1031 &self,
1032 provider: &dyn crate::provider::IDatabaseProvider,
1033 migration_id: &str,
1034 ) -> EFResult<bool> {
1035 let applied = self.get_applied_migrations(provider).await?;
1036 Ok(applied.iter().any(|e| e.migration_id == migration_id))
1037 }
1038
1039 pub async fn apply_pending(
1041 &self,
1042 provider: &dyn crate::provider::IDatabaseProvider,
1043 migrations: &[Migration],
1044 ) -> EFResult<usize> {
1045 let applied: std::collections::HashSet<String> = self
1046 .get_applied_migrations(provider)
1047 .await?
1048 .into_iter()
1049 .map(|e| e.migration_id)
1050 .collect();
1051 let mut count = 0usize;
1052 for migration in migrations {
1053 if applied.contains(&migration.id) {
1054 continue;
1055 }
1056 self.apply(provider, migration).await?;
1057 count += 1;
1058 }
1059 Ok(count)
1060 }
1061
1062 pub async fn revert_last(
1064 &self,
1065 provider: &dyn crate::provider::IDatabaseProvider,
1066 migrations: &[Migration],
1067 ) -> EFResult<Option<String>> {
1068 let applied = self.get_applied_migrations(provider).await?;
1069 let last = applied.last().map(|e| e.migration_id.clone());
1070 let Some(last_id) = last else {
1071 return Ok(None);
1072 };
1073 let migration = migrations.iter().find(|m| m.id == last_id).ok_or_else(|| {
1074 crate::error::EFError::migration(format!(
1075 "applied migration '{}' not found in local migration set",
1076 last_id
1077 ))
1078 })?;
1079 self.revert(provider, migration).await?;
1080 Ok(Some(last_id))
1081 }
1082
1083 pub async fn revert_to_target(
1090 &self,
1091 provider: &dyn crate::provider::IDatabaseProvider,
1092 migrations: &[Migration],
1093 target: Option<&str>,
1094 ) -> EFResult<Vec<String>> {
1095 let applied = self.get_applied_migrations(provider).await?;
1096 let applied_ids: Vec<&str> = applied.iter().map(|e| e.migration_id.as_str()).collect();
1097
1098 let to_revert: Vec<String> = match target {
1099 Some(t) => {
1100 let idx = applied_ids.iter().position(|id| *id == t).ok_or_else(|| {
1101 crate::error::EFError::migration(format!(
1102 "target migration '{t}' is not applied"
1103 ))
1104 })?;
1105 applied_ids[idx + 1..]
1106 .iter()
1107 .map(|s| s.to_string())
1108 .collect()
1109 }
1110 None => applied_ids.iter().map(|s| s.to_string()).collect(),
1111 };
1112
1113 let mut reverted = Vec::with_capacity(to_revert.len());
1114 for id in to_revert.iter().rev() {
1115 let migration = migrations.iter().find(|m| m.id == *id).ok_or_else(|| {
1116 crate::error::EFError::migration(format!(
1117 "applied migration '{id}' not found in local migration set"
1118 ))
1119 })?;
1120 self.revert(provider, migration).await?;
1121 reverted.push(id.clone());
1122 }
1123 Ok(reverted)
1124 }
1125
1126 pub fn generate_script(
1138 migrations: &[Migration],
1139 from: Option<&str>,
1140 to: Option<&str>,
1141 ) -> EFResult<String> {
1142 let from_idx: i64 = match from {
1145 Some(f) => migrations.iter().position(|m| m.id == f).ok_or_else(|| {
1146 crate::error::EFError::migration(format!(
1147 "from migration '{f}' not found in local set"
1148 ))
1149 })? as i64,
1150 None => -1,
1151 };
1152 let to_idx: i64 = match to {
1153 Some(t) => migrations.iter().position(|m| m.id == t).ok_or_else(|| {
1154 crate::error::EFError::migration(format!(
1155 "to migration '{t}' not found in local set"
1156 ))
1157 })? as i64,
1158 None => migrations.len() as i64 - 1,
1159 };
1160
1161 let mut sql = String::new();
1162 if from_idx < to_idx {
1163 sql.push_str("-- Migration script (forward)\n\n");
1165 let start = (from_idx + 1) as usize;
1166 let end = (to_idx + 1) as usize; for m in &migrations[start..end] {
1168 sql.push_str(&format!("-- Up: {}\n", m.id));
1169 sql.push_str(&m.up_sql.replace("{migration_id}", &m.id));
1170 sql.push('\n');
1171 }
1172 } else if from_idx > to_idx {
1173 sql.push_str("-- Migration script (reverse)\n\n");
1175 let start = to_idx as usize + 1; let end = from_idx as usize + 1; for m in migrations[start..end].iter().rev() {
1178 sql.push_str(&format!("-- Down: {}\n", m.id));
1179 sql.push_str(&m.down_sql.replace("{migration_id}", &m.id));
1180 sql.push('\n');
1181 }
1182 } else {
1183 sql.push_str("-- Nothing to do (from == to)\n");
1184 }
1185 Ok(sql)
1186 }
1187
1188 pub async fn ensure_created(
1192 &self,
1193 provider: &dyn crate::provider::IDatabaseProvider,
1194 entity_types: &[EntityTypeMeta],
1195 ) -> EFResult<()> {
1196 let snapshot = self.create_snapshot("__ensure_created__", entity_types);
1197 let changes = self.initial_create(&snapshot);
1198 let sql = self.generate_ddl_sql(&changes);
1199 for statement in split_sql_statements(&sql) {
1200 if !statement.is_empty() {
1201 provider.execute_migration_command(&statement).await?;
1202 }
1203 }
1204 Ok(())
1205 }
1206
1207 pub async fn ensure_deleted(
1210 &self,
1211 provider: &dyn crate::provider::IDatabaseProvider,
1212 entity_types: &[EntityTypeMeta],
1213 ) -> EFResult<()> {
1214 let q = |s: &str| self.dialect.quote(s);
1215 for meta in entity_types {
1216 let sql = format!("DROP TABLE IF EXISTS {};", q(meta.table_name.as_ref()));
1217 provider.execute_migration_command(&sql).await?;
1218 }
1219 Ok(())
1220 }
1221
1222 pub async fn apply_seed_data(
1224 &self,
1225 provider: &dyn crate::provider::IDatabaseProvider,
1226 meta: &EntityTypeMeta,
1227 rows: &[std::collections::HashMap<String, crate::provider::DbValue>],
1228 ) -> EFResult<()> {
1229 if rows.is_empty() {
1230 return Ok(());
1231 }
1232
1233 let gen = provider.sql_generator();
1234 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
1235 if scalar_props.is_empty() {
1236 return Ok(());
1237 }
1238
1239 let col_names: Vec<&str> = scalar_props
1240 .iter()
1241 .map(|p| p.column_name.as_ref())
1242 .collect();
1243
1244 let mut conn = provider.get_connection().await?;
1245 for row in rows {
1246 let params: Vec<crate::provider::DbValue> = scalar_props
1247 .iter()
1248 .map(|p| {
1249 row.get(p.field_name.as_ref())
1250 .cloned()
1251 .unwrap_or(crate::provider::DbValue::Null)
1252 })
1253 .collect();
1254
1255 let sql = seed_insert_sql(self.dialect, meta.table_name.as_ref(), &col_names, gen);
1256 conn.execute(&sql, ¶ms).await?;
1257 }
1258 Ok(())
1259 }
1260}
1261
1262#[derive(Debug, Clone)]
1266pub struct MigrationHistoryEntry {
1267 pub migration_id: String,
1268 pub product_version: String,
1269}
1270
1271pub const MIGRATION_HISTORY_TABLE: &str = "__ef_migrations_history";
1273
1274pub const PRODUCT_VERSION: &str = env!("CARGO_PKG_VERSION");
1276
1277fn seed_insert_sql(
1278 dialect: MigrationDialect,
1279 table: &str,
1280 columns: &[&str],
1281 gen: &dyn crate::provider::ISqlGenerator,
1282) -> String {
1283 let quoted_table = dialect.quote(table);
1284 let quoted_cols: Vec<String> = columns.iter().map(|c| dialect.quote(c)).collect();
1285 let placeholders: Vec<String> = (0..columns.len())
1286 .map(|i| gen.parameter_placeholder(i + 1))
1287 .collect();
1288 let cols = quoted_cols.join(", ");
1289 let vals = placeholders.join(", ");
1290 match dialect {
1291 MigrationDialect::Sqlite => {
1292 format!("INSERT OR IGNORE INTO {quoted_table} ({cols}) VALUES ({vals})")
1293 }
1294 MigrationDialect::Postgres => {
1295 format!("INSERT INTO {quoted_table} ({cols}) VALUES ({vals}) ON CONFLICT DO NOTHING")
1296 }
1297 MigrationDialect::MySql => {
1298 format!("INSERT IGNORE INTO {quoted_table} ({cols}) VALUES ({vals})")
1299 }
1300 }
1301}
1302
1303fn split_sql_statements(sql: &str) -> Vec<String> {
1305 sql.lines()
1306 .filter(|l| {
1307 let trimmed = l.trim();
1308 !trimmed.is_empty() && !trimmed.starts_with("--")
1309 })
1310 .collect::<Vec<_>>()
1311 .join("\n")
1312 .split(';')
1313 .map(|s| s.trim().to_string())
1314 .filter(|s| !s.is_empty())
1315 .collect()
1316}
1317
1318pub fn create_migration_history_table_sql(dialect: MigrationDialect) -> String {
1320 match dialect {
1321 MigrationDialect::Postgres => format!(
1322 r#"CREATE TABLE IF NOT EXISTS "{table}" (
1323 "migration_id" VARCHAR(150) NOT NULL PRIMARY KEY,
1324 "product_version" VARCHAR(32) NOT NULL,
1325 "applied_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1326);"#,
1327 table = MIGRATION_HISTORY_TABLE
1328 ),
1329 MigrationDialect::MySql => format!(
1330 r#"CREATE TABLE IF NOT EXISTS `{table}` (
1331 `migration_id` VARCHAR(150) NOT NULL PRIMARY KEY,
1332 `product_version` VARCHAR(32) NOT NULL,
1333 `applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
1334) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"#,
1335 table = MIGRATION_HISTORY_TABLE
1336 ),
1337 MigrationDialect::Sqlite => format!(
1338 r#"CREATE TABLE IF NOT EXISTS "{table}" (
1339 "migration_id" TEXT NOT NULL PRIMARY KEY,
1340 "product_version" TEXT NOT NULL
1341);"#,
1342 table = MIGRATION_HISTORY_TABLE
1343 ),
1344 }
1345}
1346
1347use std::fs;
1352use std::path::{Path, PathBuf};
1353
1354#[derive(Debug, Clone)]
1364pub struct MigrationStore {
1365 root: PathBuf,
1366}
1367
1368impl MigrationStore {
1369 pub fn new(root: impl Into<PathBuf>) -> Self {
1370 Self { root: root.into() }
1371 }
1372
1373 pub fn root(&self) -> &Path {
1374 &self.root
1375 }
1376
1377 pub fn save(&self, migration: &Migration) -> EFResult<()> {
1379 fs::create_dir_all(&self.root).map_err(migration_io_err)?;
1380 let dir = self.root.join(&migration.id);
1381 fs::create_dir_all(&dir).map_err(migration_io_err)?;
1382 fs::write(dir.join("up.sql"), &migration.up_sql).map_err(migration_io_err)?;
1383 fs::write(dir.join("down.sql"), &migration.down_sql).map_err(migration_io_err)?;
1384 Ok(())
1385 }
1386
1387 pub fn load_all(&self) -> EFResult<Vec<Migration>> {
1389 if !self.root.exists() {
1390 return Ok(Vec::new());
1391 }
1392 let mut ids: Vec<String> = fs::read_dir(&self.root)
1393 .map_err(migration_io_err)?
1394 .filter_map(|e| e.ok())
1395 .filter(|e| e.path().is_dir())
1396 .filter_map(|e| e.file_name().into_string().ok())
1397 .collect();
1398 ids.sort();
1399 ids.into_iter().map(|id| self.load(&id)).collect()
1400 }
1401
1402 pub fn load(&self, id: &str) -> EFResult<Migration> {
1403 let dir = self.root.join(id);
1404 let up_sql = fs::read_to_string(dir.join("up.sql")).map_err(migration_io_err)?;
1405 let down_sql = fs::read_to_string(dir.join("down.sql")).map_err(migration_io_err)?;
1406 Ok(Migration {
1407 id: id.to_string(),
1408 description: id.to_string(),
1409 up_sql,
1410 down_sql,
1411 })
1412 }
1413
1414 pub fn save_snapshot(&self, snapshot: &ModelSnapshot) -> EFResult<()> {
1416 fs::create_dir_all(&self.root).map_err(migration_io_err)?;
1417 let json = snapshot_to_json(snapshot);
1418 fs::write(self.root.join("model_snapshot.json"), json).map_err(migration_io_err)
1419 }
1420
1421 pub fn load_snapshot(&self) -> EFResult<Option<ModelSnapshot>> {
1422 let path = self.root.join("model_snapshot.json");
1423 if !path.exists() {
1424 return Ok(None);
1425 }
1426 let text = fs::read_to_string(path).map_err(migration_io_err)?;
1427 parse_model_snapshot_json(&text)
1428 }
1429}
1430
1431pub fn parse_model_snapshot_json(text: &str) -> EFResult<Option<ModelSnapshot>> {
1433 snapshot_from_json(text)
1434}
1435
1436fn migration_io_err(e: std::io::Error) -> crate::error::EFError {
1437 crate::error::EFError::migration(e.to_string())
1438}
1439
1440fn snapshot_to_json(snapshot: &ModelSnapshot) -> String {
1441 let mut out = String::from("{\n \"migration_id\": \"");
1442 out.push_str(&snapshot.migration_id.replace('"', "\\\""));
1443 out.push_str("\",\n \"entity_types\": [\n");
1444 for (i, et) in snapshot.entity_types.iter().enumerate() {
1445 if i > 0 {
1446 out.push(',');
1447 }
1448 out.push_str(" {\n");
1449 out.push_str(&format!(
1450 " \"type_name\": \"{}\",\n",
1451 et.type_name.replace('"', "\\\"")
1452 ));
1453 out.push_str(&format!(
1454 " \"table_name\": \"{}\",\n",
1455 et.table_name.replace('"', "\\\"")
1456 ));
1457 out.push_str(" \"columns\": [\n");
1458 for (j, col) in et.columns.iter().enumerate() {
1459 if j > 0 {
1460 out.push(',');
1461 }
1462 out.push_str(&format!(
1463 " {{\"field_name\":\"{}\",\"column_name\":\"{}\",\"type_name\":\"{}\",\"is_primary_key\":{},\"is_required\":{},\"is_foreign_key\":{},\"max_length\":{},\"is_auto_increment\":{},\"fk_referenced_table\":{},\"fk_referenced_column\":{},\"has_index\":{},\"is_unique\":{}}}\n",
1464 col.field_name.replace('"', "\\\""),
1465 col.column_name.replace('"', "\\\""),
1466 col.type_name.replace('"', "\\\""),
1467 col.is_primary_key,
1468 col.is_required,
1469 col.is_foreign_key,
1470 col.max_length.map(|n| n.to_string()).unwrap_or_else(|| "null".into()),
1471 col.is_auto_increment,
1472 col.fk_referenced_table
1473 .as_ref()
1474 .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
1475 .unwrap_or_else(|| "null".into()),
1476 col.fk_referenced_column
1477 .as_ref()
1478 .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
1479 .unwrap_or_else(|| "null".into()),
1480 col.has_index,
1481 col.is_unique
1482 ));
1483 }
1484 out.push_str(" ]\n }\n");
1485 }
1486 out.push_str(" ]\n}\n");
1487 out
1488}
1489
1490fn snapshot_from_json(text: &str) -> EFResult<Option<ModelSnapshot>> {
1491 let migration_id =
1493 extract_json_string(text, "migration_id").unwrap_or_else(|| "__snapshot__".to_string());
1494 let mut entity_types = Vec::new();
1495 if let Some(arr_start) = text.find("\"entity_types\"") {
1496 let slice = &text[arr_start..];
1497 for table_block in slice.split("\"table_name\"").skip(1) {
1498 let table_name = extract_quoted_after_colon(table_block).unwrap_or_default();
1499 let _type_name = entity_types.len().to_string(); let columns_start = table_block.find("\"columns\"");
1501 let mut columns = Vec::new();
1502 if let Some(cs) = columns_start {
1503 let col_slice = &table_block[cs..];
1504 for col_chunk in col_slice.split("\"field_name\"").skip(1) {
1505 if let Some(field_name) = extract_quoted_after_colon(col_chunk) {
1506 let column_name = extract_json_string(col_chunk, "column_name")
1507 .unwrap_or_else(|| field_name.clone());
1508 let type_name_col = extract_json_string(col_chunk, "type_name")
1509 .unwrap_or_else(|| "String".to_string());
1510 columns.push(SnapshotColumn {
1511 field_name,
1512 column_name,
1513 type_name: type_name_col,
1514 is_primary_key: col_chunk.contains("\"is_primary_key\":true"),
1515 is_required: col_chunk.contains("\"is_required\":true"),
1516 is_foreign_key: col_chunk.contains("\"is_foreign_key\":true"),
1517 max_length: None,
1518 is_auto_increment: col_chunk.contains("\"is_auto_increment\":true"),
1519 fk_referenced_table: extract_json_string(
1520 col_chunk,
1521 "fk_referenced_table",
1522 ),
1523 fk_referenced_column: extract_json_string(
1524 col_chunk,
1525 "fk_referenced_column",
1526 ),
1527 has_index: col_chunk.contains("\"has_index\":true"),
1528 is_unique: col_chunk.contains("\"is_unique\":true"),
1529 });
1530 }
1531 }
1532 }
1533 if !table_name.is_empty() {
1534 entity_types.push(SnapshotEntityType {
1535 type_name: table_name.clone(),
1536 table_name,
1537 columns,
1538 });
1539 }
1540 }
1541 }
1542 if entity_types.is_empty() {
1543 return Ok(None);
1544 }
1545 Ok(Some(ModelSnapshot {
1546 migration_id,
1547 entity_types,
1548 }))
1549}
1550
1551fn extract_json_string(haystack: &str, key: &str) -> Option<String> {
1552 let needle = format!("\"{key}\"");
1553 let pos = haystack.find(&needle)?;
1554 extract_quoted_after_colon(&haystack[pos + needle.len()..])
1555}
1556
1557fn extract_quoted_after_colon(s: &str) -> Option<String> {
1558 let colon = s.find(':')?;
1559 let rest = s[colon + 1..].trim_start();
1560 if !rest.starts_with('"') {
1561 return None;
1562 }
1563 let inner = &rest[1..];
1564 let end = inner.find('"')?;
1565 Some(inner[..end].to_string())
1566}