cufflink_types/manifest/
migration.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
5pub struct SqlMigration {
6 pub version: u32,
7 pub name: String,
8 pub sql: String,
11 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
14 pub no_transaction: bool,
15}
16
17impl SqlMigration {
18 pub const NO_TRANSACTION_MARKER: &'static str = "-- cufflink:no-transaction";
19
20 pub fn declares_no_transaction(sql: &str) -> bool {
22 sql.trim_start_matches('\u{feff}')
23 .lines()
24 .next()
25 .is_some_and(|line| line.trim() == Self::NO_TRANSACTION_MARKER)
26 }
27
28 pub fn file_stem(&self) -> String {
30 format!("V{}__{}", self.version, self.name)
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct MigrationHistory {
38 pub table: String,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub baseline: Option<u32>,
43 #[serde(default, skip_serializing_if = "MigrationLock::is_platform")]
44 pub lock: MigrationLock,
45 #[serde(default, skip_serializing_if = "HistoryMode::is_apply")]
46 pub mode: HistoryMode,
47}
48
49impl MigrationHistory {
50 pub const DEFAULT_TABLE: &'static str = "refinery_schema_history";
52}
53
54impl Default for MigrationHistory {
55 fn default() -> Self {
56 Self {
57 table: Self::DEFAULT_TABLE.to_string(),
58 baseline: None,
59 lock: MigrationLock::default(),
60 mode: HistoryMode::default(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum MigrationLock {
69 #[default]
70 Platform,
71 LegacyTableHash,
74}
75
76impl MigrationLock {
77 pub fn is_platform(&self) -> bool {
78 *self == MigrationLock::Platform
79 }
80}
81
82#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum HistoryMode {
85 #[default]
86 Apply,
87 VerifyOnly,
90}
91
92impl HistoryMode {
93 pub fn is_apply(&self) -> bool {
94 *self == HistoryMode::Apply
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use serde_json::json;
102
103 #[test]
104 fn a_migration_keeps_its_bytes() {
105 let sql = "\u{feff}CREATE TABLE a (\r\n id INT\r\n);\r\n";
106 let migration = SqlMigration {
107 version: 6,
108 name: "add_note_kind".into(),
109 sql: sql.into(),
110 no_transaction: false,
111 };
112 let wire = json!({"version": 6, "name": "add_note_kind", "sql": sql});
113 assert_eq!(serde_json::to_value(&migration).unwrap(), wire);
114 let parsed: SqlMigration = serde_json::from_value(wire).unwrap();
115 assert_eq!(parsed.sql.as_bytes(), sql.as_bytes());
116 assert_eq!(parsed.file_stem(), "V6__add_note_kind");
117 }
118
119 #[test]
120 fn the_marker_is_the_first_line() {
121 for sql in [
122 "-- cufflink:no-transaction\nCREATE INDEX CONCURRENTLY i ON t (c);",
123 "-- cufflink:no-transaction",
124 " -- cufflink:no-transaction \r\nCREATE INDEX CONCURRENTLY i ON t (c);",
125 "\u{feff}-- cufflink:no-transaction\nSELECT 1;",
126 ] {
127 assert!(SqlMigration::declares_no_transaction(sql), "{sql:?}");
128 }
129 for sql in [
130 "",
131 "SELECT 1;\n-- cufflink:no-transaction",
132 "\n-- cufflink:no-transaction\nSELECT 1;",
133 "-- cufflink:no-transaction please",
134 "--cufflink:no-transaction",
135 ] {
136 assert!(!SqlMigration::declares_no_transaction(sql), "{sql:?}");
137 }
138 let migration = SqlMigration {
139 version: 7,
140 name: "callback_index".into(),
141 sql: "-- cufflink:no-transaction\nSELECT 1;".into(),
142 no_transaction: true,
143 };
144 assert_eq!(
145 serde_json::to_value(&migration).unwrap()["no_transaction"],
146 json!(true)
147 );
148 }
149
150 #[test]
151 fn history_round_trips() {
152 let default = MigrationHistory::default();
153 assert_eq!(
154 serde_json::to_value(&default).unwrap(),
155 json!({"table": "refinery_schema_history"})
156 );
157 let legacy = MigrationHistory {
158 table: "public.schema_history_deferred".into(),
159 baseline: Some(15),
160 lock: MigrationLock::LegacyTableHash,
161 mode: HistoryMode::VerifyOnly,
162 };
163 let wire = json!({
164 "table": "public.schema_history_deferred",
165 "baseline": 15,
166 "lock": "legacy_table_hash",
167 "mode": "verify_only"
168 });
169 assert_eq!(serde_json::to_value(&legacy).unwrap(), wire);
170 assert_eq!(
171 serde_json::from_value::<MigrationHistory>(wire).unwrap(),
172 legacy
173 );
174 }
175}