@@ -0,0 +1,293 @@
1+use reshape::migrations::{AddColumn, Column, CreateTable, Migration};
2+use reshape::Status;
3+
4+mod common;
5+
6+#[test]
7+fn add_column() {
8+ let (mut reshape, mut old_db, mut new_db) = common::setup();
9+
10+ let create_users_table = Migration::new("create_users_table", None).with_action(CreateTable {
11+ name: "users".to_string(),
12+ columns: vec![
13+ Column {
14+ name: "id".to_string(),
15+ data_type: "SERIAL".to_string(),
16+ nullable: true,
17+ default: None,
18+ },
19+ Column {
20+ name: "name".to_string(),
21+ data_type: "TEXT".to_string(),
22+ nullable: false,
23+ default: None,
24+ },
25+ ],
26+ });
27+ let add_first_last_name_columns = Migration::new("add_first_and_last_name_columns", None)
28+ .with_action(AddColumn {
29+ table: "users".to_string(),
30+ column: Column {
31+ name: "first".to_string(),
32+ data_type: "TEXT".to_string(),
33+ nullable: false,
34+ default: None,
35+ },
36+ up: Some("(STRING_TO_ARRAY(name, ' '))[1]".to_string()),
37+ })
38+ .with_action(AddColumn {
39+ table: "users".to_string(),
40+ column: Column {
41+ name: "last".to_string(),
42+ data_type: "TEXT".to_string(),
43+ nullable: false,
44+ default: None,
45+ },
46+ up: Some("(STRING_TO_ARRAY(name, ' '))[2]".to_string()),
47+ });
48+
49+ let first_migrations = vec![create_users_table.clone()];
50+ let second_migrations = vec![
51+ create_users_table.clone(),
52+ add_first_last_name_columns.clone(),
53+ ];
54+
55+ // Run first migration, should automatically finish
56+ reshape.migrate(first_migrations.clone()).unwrap();
57+ assert!(matches!(reshape.state.status, Status::Idle));
58+ assert_eq!(
59+ Some(&create_users_table.name),
60+ reshape.state.current_migration.as_ref()
61+ );
62+
63+ // Update search paths
64+ old_db
65+ .simple_query(&reshape::generate_schema_query(
66+ &first_migrations.last().unwrap().name,
67+ ))
68+ .unwrap();
69+ new_db
70+ .simple_query(&reshape::generate_schema_query(
71+ &first_migrations.last().unwrap().name,
72+ ))
73+ .unwrap();
74+
75+ // Insert some test users
76+ new_db
77+ .simple_query(
78+ "
79+ INSERT INTO users (id, name) VALUES
80+ (1, 'John Doe'),
81+ (2, 'Jane Doe');
82+ ",
83+ )
84+ .unwrap();
85+
86+ // Run second migration
87+ reshape.migrate(second_migrations.clone()).unwrap();
88+ new_db
89+ .simple_query(&reshape::generate_schema_query(
90+ &second_migrations.last().unwrap().name,
91+ ))
92+ .unwrap();
93+
94+ // Check that the existing users have the new columns populated
95+ let expected = vec![("John", "Doe"), ("Jane", "Doe")];
96+ assert!(new_db
97+ .query("SELECT first, last FROM users ORDER BY id", &[],)
98+ .unwrap()
99+ .iter()
100+ .map(|row| (row.get("first"), row.get("last")))
101+ .eq(expected));
102+
103+ // Insert data using old schema and make sure the new columns are populated
104+ old_db
105+ .simple_query("INSERT INTO users (id, name) VALUES (3, 'Test Testsson')")
106+ .unwrap();
107+ let (first_name, last_name): (String, String) = new_db
108+ .query_one("SELECT first, last from users WHERE id = 3", &[])
109+ .map(|row| (row.get("first"), row.get("last")))
110+ .unwrap();
111+ assert_eq!(
112+ ("Test", "Testsson"),
113+ (first_name.as_ref(), last_name.as_ref())
114+ );
115+
116+ reshape.complete_migration().unwrap();
117+}
118+
119+#[test]
120+fn add_column_nullable() {
121+ let (mut reshape, mut old_db, mut new_db) = common::setup();
122+
123+ let create_users_table = Migration::new("create_users_table", None).with_action(CreateTable {
124+ name: "users".to_string(),
125+ columns: vec![Column {
126+ name: "id".to_string(),
127+ data_type: "SERIAL".to_string(),
128+ nullable: true,
129+ default: None,
130+ }],
131+ });
132+ let add_name_column = Migration::new("add_nullable_name_column", None).with_action(AddColumn {
133+ table: "users".to_string(),
134+ column: Column {
135+ name: "name".to_string(),
136+ data_type: "TEXT".to_string(),
137+ nullable: true,
138+ default: None,
139+ },
140+ up: None,
141+ });
142+
143+ let first_migrations = vec![create_users_table.clone()];
144+ let second_migrations = vec![create_users_table.clone(), add_name_column.clone()];
145+
146+ // Run first migration, should automatically finish
147+ reshape.migrate(first_migrations.clone()).unwrap();
148+ assert!(matches!(reshape.state.status, Status::Idle));
149+ assert_eq!(
150+ Some(&create_users_table.name),
151+ reshape.state.current_migration.as_ref()
152+ );
153+
154+ // Update search paths
155+ old_db
156+ .simple_query(&reshape::generate_schema_query(
157+ &first_migrations.last().unwrap().name,
158+ ))
159+ .unwrap();
160+ new_db
161+ .simple_query(&reshape::generate_schema_query(
162+ &first_migrations.last().unwrap().name,
163+ ))
164+ .unwrap();
165+
166+ // Insert some test values
167+ new_db
168+ .simple_query(
169+ "
170+ INSERT INTO users (id) VALUES
171+ (1),
172+ (2);
173+ ",
174+ )
175+ .unwrap();
176+
177+ // Run second migration
178+ reshape.migrate(second_migrations.clone()).unwrap();
179+ new_db
180+ .simple_query(&reshape::generate_schema_query(
181+ &second_migrations.last().unwrap().name,
182+ ))
183+ .unwrap();
184+
185+ // Ensure existing data got updated
186+ let expected: Vec<Option<String>> = vec![None, None];
187+ assert!(new_db
188+ .query("SELECT name FROM users ORDER BY id", &[],)
189+ .unwrap()
190+ .iter()
191+ .map(|row| row.get::<_, Option<String>>("name"))
192+ .eq(expected));
193+
194+ // Insert data using old schema and ensure new column is NULL
195+ old_db
196+ .simple_query("INSERT INTO users (id) VALUES (3)")
197+ .unwrap();
198+ let name: Option<String> = new_db
199+ .query_one("SELECT name from users WHERE id = 3", &[])
200+ .map(|row| (row.get("name")))
201+ .unwrap();
202+ assert_eq!(None, name);
203+
204+ // Ensure data can be inserted against new schema
205+ new_db
206+ .simple_query("INSERT INTO users (id, name) VALUES (3, 'Test Testsson'), (4, NULL)")
207+ .unwrap();
208+
209+ reshape.complete_migration().unwrap();
210+}
211+
212+#[test]
213+fn add_column_with_default() {
214+ let (mut reshape, mut old_db, mut new_db) = common::setup();
215+
216+ let create_users_table = Migration::new("create_users_table", None).with_action(CreateTable {
217+ name: "users".to_string(),
218+ columns: vec![Column {
219+ name: "id".to_string(),
220+ data_type: "SERIAL".to_string(),
221+ nullable: true,
222+ default: None,
223+ }],
224+ });
225+ let add_name_column =
226+ Migration::new("add_name_column_with_default", None).with_action(AddColumn {
227+ table: "users".to_string(),
228+ column: Column {
229+ name: "name".to_string(),
230+ data_type: "TEXT".to_string(),
231+ nullable: false,
232+ default: Some("'DEFAULT'".to_string()),
233+ },
234+ up: None,
235+ });
236+
237+ let first_migrations = vec![create_users_table.clone()];
238+ let second_migrations = vec![create_users_table.clone(), add_name_column.clone()];
239+
240+ // Run first migration, should automatically finish
241+ reshape.migrate(first_migrations.clone()).unwrap();
242+ assert!(matches!(reshape.state.status, Status::Idle));
243+ assert_eq!(
244+ Some(&create_users_table.name),
245+ reshape.state.current_migration.as_ref()
246+ );
247+
248+ // Update search paths
249+ old_db
250+ .simple_query(&reshape::generate_schema_query(
251+ &first_migrations.last().unwrap().name,
252+ ))
253+ .unwrap();
254+ new_db
255+ .simple_query(&reshape::generate_schema_query(
256+ &first_migrations.last().unwrap().name,
257+ ))
258+ .unwrap();
259+
260+ // Insert some test values
261+ new_db
262+ .simple_query("INSERT INTO users (id) VALUES (1), (2)")
263+ .unwrap();
264+
265+ // Run second migration
266+ reshape.migrate(second_migrations.clone()).unwrap();
267+ new_db
268+ .simple_query(&reshape::generate_schema_query(
269+ &second_migrations.last().unwrap().name,
270+ ))
271+ .unwrap();
272+
273+ // Ensure existing data got updated with defaults
274+ let expected = vec!["DEFAULT".to_string(), "DEFAULT".to_string()];
275+ assert!(new_db
276+ .query("SELECT name FROM users ORDER BY id", &[],)
277+ .unwrap()
278+ .iter()
279+ .map(|row| row.get::<_, String>("name"))
280+ .eq(expected));
281+
282+ // Insert data using old schema and ensure new column gets the default value
283+ old_db
284+ .simple_query("INSERT INTO users (id) VALUES (3)")
285+ .unwrap();
286+ let name: String = new_db
287+ .query_one("SELECT name from users WHERE id = 3", &[])
288+ .map(|row| row.get("name"))
289+ .unwrap();
290+ assert_eq!("DEFAULT", name);
291+
292+ reshape.complete_migration().unwrap();
293+}