Skip to main content

toasty_core/schema/db/
migration.rs

1/// A database migration generated from a [`diff::Schema`](super::super::diff::Schema) by a driver.
2///
3/// Currently only SQL migrations are supported. Multiple SQL statements
4/// within a single migration are separated by breakpoint markers
5/// (`-- #[toasty::breakpoint]`).
6///
7/// # Examples
8///
9/// ```ignore
10/// use toasty_core::schema::db::Migration;
11///
12/// let m = Migration::new_sql("CREATE TABLE users (id INTEGER PRIMARY KEY)".to_string());
13/// assert_eq!(m.statements(), vec!["CREATE TABLE users (id INTEGER PRIMARY KEY)"]);
14/// ```
15#[derive(Debug)]
16pub enum Migration {
17    /// A SQL migration containing one or more statements.
18    Sql(String),
19}
20
21impl Migration {
22    /// Creates a SQL migration from a single SQL string.
23    pub fn new_sql(sql: String) -> Self {
24        Migration::Sql(sql)
25    }
26
27    /// Creates a SQL migration from multiple SQL statements.
28    /// Statements are joined with `-- #[toasty::breakpoint]` markers.
29    pub fn new_sql_with_breakpoints<S: AsRef<str>>(statements: &[S]) -> Self {
30        let sql = statements
31            .iter()
32            .map(|s| s.as_ref())
33            .collect::<Vec<_>>()
34            .join(&format!("\n{BREAKPOINT}\n"));
35        Migration::Sql(sql)
36    }
37
38    /// Returns individual SQL statements by splitting on breakpoint markers.
39    pub fn statements(&self) -> Vec<&str> {
40        match self {
41            Migration::Sql(sql) => split_on_breakpoints(sql),
42        }
43    }
44}
45
46/// The comment that separates statements within a SQL migration.
47const BREAKPOINT: &str = "-- #[toasty::breakpoint]";
48
49/// Splits `sql` on lines that consist of a breakpoint marker.
50///
51/// Neither the line ending before a marker nor the marker's own line ending
52/// belongs to a statement, whether it is `\n` or `\r\n`. Pieces that hold
53/// only whitespace, such as the one after a trailing marker, are dropped.
54fn split_on_breakpoints(sql: &str) -> Vec<&str> {
55    let mut statements = Vec::new();
56    let mut statement_start = 0;
57    let mut line_start = 0;
58
59    for line in sql.split_inclusive('\n') {
60        let line_end = line_start + line.len();
61
62        if line.trim() == BREAKPOINT {
63            statements.push(strip_line_ending(&sql[statement_start..line_start]));
64            statement_start = line_end;
65        }
66
67        line_start = line_end;
68    }
69
70    statements.push(&sql[statement_start..]);
71    statements.retain(|s| !s.trim().is_empty());
72    statements
73}
74
75fn strip_line_ending(s: &str) -> &str {
76    s.strip_suffix("\r\n")
77        .or_else(|| s.strip_suffix('\n'))
78        .unwrap_or(s)
79}
80
81/// Metadata about a migration that has already been applied to a database.
82///
83/// Stores the unique migration ID assigned by the migration system.
84///
85/// # Examples
86///
87/// ```ignore
88/// use toasty_core::schema::db::AppliedMigration;
89///
90/// let applied = AppliedMigration::new(42);
91/// assert_eq!(applied.id(), 42);
92/// ```
93pub struct AppliedMigration {
94    id: u64,
95}
96
97impl AppliedMigration {
98    /// Creates a new `AppliedMigration` with the given ID.
99    pub fn new(id: u64) -> Self {
100        Self { id }
101    }
102
103    /// Returns the migration's unique ID.
104    pub fn id(&self) -> u64 {
105        self.id
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    const CREATE_TABLE: &str = "CREATE TABLE users (id INTEGER PRIMARY KEY);";
114    const CREATE_INDEX: &str = "CREATE INDEX idx ON users (id);";
115
116    #[test]
117    fn generated_breakpoints_round_trip() {
118        let m = Migration::new_sql_with_breakpoints(&[CREATE_TABLE, CREATE_INDEX]);
119        let Migration::Sql(sql) = &m;
120        assert_eq!(
121            sql,
122            "CREATE TABLE users (id INTEGER PRIMARY KEY);\n-- #[toasty::breakpoint]\nCREATE INDEX idx ON users (id);"
123        );
124        assert_eq!(m.statements(), vec![CREATE_TABLE, CREATE_INDEX]);
125    }
126
127    #[test]
128    fn splits_on_lf_and_crlf_breakpoints() {
129        // A multi-line CRLF statement, an LF breakpoint, a CRLF breakpoint,
130        // and a trailing newline on the final statement.
131        let sql = "CREATE TABLE users (\r\n    id INTEGER PRIMARY KEY\r\n);\r\n\
132                   -- #[toasty::breakpoint]\n\
133                   CREATE INDEX idx ON users (id);\n\
134                   -- #[toasty::breakpoint]\r\n\
135                   SELECT 1;\r\n";
136        let m = Migration::new_sql(sql.to_string());
137        assert_eq!(
138            m.statements(),
139            vec![
140                "CREATE TABLE users (\r\n    id INTEGER PRIMARY KEY\r\n);",
141                CREATE_INDEX,
142                "SELECT 1;\r\n",
143            ]
144        );
145    }
146
147    #[test]
148    fn only_whole_marker_lines_split() {
149        // Whitespace around the marker is ignored, but a marker inside a
150        // string literal or after a statement on the same line is not a split.
151        let sql = format!(
152            "SELECT '-- #[toasty::breakpoint]';\n  -- #[toasty::breakpoint]  \n{CREATE_INDEX} -- #[toasty::breakpoint]"
153        );
154        let m = Migration::new_sql(sql);
155        assert_eq!(
156            m.statements(),
157            vec![
158                "SELECT '-- #[toasty::breakpoint]';",
159                "CREATE INDEX idx ON users (id); -- #[toasty::breakpoint]",
160            ]
161        );
162    }
163
164    #[test]
165    fn drops_empty_statements() {
166        // Markers at the start, doubled, and at the end leave no empty pieces.
167        let sql = format!(
168            "-- #[toasty::breakpoint]\n{CREATE_TABLE}\n-- #[toasty::breakpoint]\n\n-- #[toasty::breakpoint]\n{CREATE_INDEX}\n-- #[toasty::breakpoint]\n"
169        );
170        let m = Migration::new_sql(sql);
171        assert_eq!(m.statements(), vec![CREATE_TABLE, CREATE_INDEX]);
172        assert!(Migration::new_sql(String::new()).statements().is_empty());
173    }
174}