use crate::util::confirm;
use rusqlite::Transaction;
use rusqlite_migration::{HookError, HookResult, M, Migrations};
pub fn migrations() -> Migrations<'static> {
Migrations::new(vec![
M::up(
"CREATE TABLE collections (
id UUID PRIMARY KEY NOT NULL,
path BLOB NOT NULL UNIQUE
)",
),
M::up(
"CREATE TABLE requests (
id UUID PRIMARY KEY NOT NULL,
collection_id UUID NOT NULL,
profile_id TEXT,
recipe_id TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
request BLOB NOT NULL,
response BLOB NOT NULL,
status_code INTEGER NOT NULL,
FOREIGN KEY(collection_id) REFERENCES collections(id)
)",
),
M::up(
"CREATE TABLE ui_state (
key BLOB NOT NULL,
collection_id UUID NOT NULL,
value BLOB NOT NULL,
PRIMARY KEY (key, collection_id),
FOREIGN KEY(collection_id) REFERENCES collections(id)
)",
),
M::up("DELETE FROM requests; DELETE FROM ui_state;"),
M::up_with_hook(
"CREATE TABLE requests_v2 (
id UUID PRIMARY KEY NOT NULL,
collection_id UUID NOT NULL,
profile_id TEXT,
recipe_id TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
method TEXT NOT NULL,
url TEXT_NOT NULL,
request_headers BLOB NOT NULL,
request_body BLOB,
status_code INTEGER NOT NULL,
response_headers BLOB NOT NULL,
response_body BLOB NOT NULL,
FOREIGN KEY(collection_id) REFERENCES collections(id)
)",
migrate_requests_v2,
),
M::up(
"CREATE TABLE ui_state_v2 (
collection_id UUID NOT NULL,
key_type TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (collection_id, key_type, key),
FOREIGN KEY(collection_id) REFERENCES collections(id)
)",
),
M::up("DROP TABLE IF EXISTS requests; DROP TABLE IF EXISTS ui_state"),
M::up(
"ALTER TABLE requests_v2 ADD COLUMN \
http_version TEXT NOT NULL DEFAULT 'HTTP/1.1'",
)
.down("ALTER TABLE requests_v2 DROP COLUMN http_version"),
M::up("ALTER TABLE collections ADD COLUMN name TEXT")
.down("ALTER TABLE collections DROP COLUMN name"),
M::up(
"CREATE TABLE commands (
collection_id UUID NOT NULL,
command TEXT NOT NULL,
time TEXT NOT NULL,
PRIMARY KEY (collection_id, command),
FOREIGN KEY(collection_id) REFERENCES collections(id)
)",
)
.down("DROP TABLE IF EXISTS commands"),
M::up(
"ALTER TABLE requests_v2 \
ADD COLUMN request_body_kind INTEGER NOT NULL DEFAULT 0;
UPDATE requests_v2 SET request_body_kind = request_body IS NOT NULL",
)
.down("ALTER TABLE requests_v2 DROP COLUMN request_body_kind"),
])
}
fn migrate_requests_v2(transaction: &Transaction) -> HookResult {
let old_requests_count =
transaction.query_row("SELECT COUNT(*) FROM requests", (), |row| {
row.get::<_, u32>(0)
})?;
if old_requests_count > 0 {
let delete = confirm(
"You are upgrading from Slumber <1.8.0 to Slumber >=3.0.0. \
Your request history database contains old requests that \
cannot be migrated directly to a newer format. You can proceed \
with the upgrade by DELETING THE OLD REQUESTS now, or you can \
retain the requests by upgrading to an intermediate version \
first.\nWould you like to DELETE YOUR REQUEST HISTORY?",
);
if delete {
Ok(())
} else {
Err(HookError::Hook(
"Migration aborted. Upgrade to a version earlier than 3.0.0 \
first to retain your request history."
.into(),
))
}
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
collection::RecipeId, database::CollectionId, http::RequestId,
};
use chrono::Utc;
use rusqlite::{Connection, named_params};
use slumber_util::Factory;
#[test]
fn test_migrate_latest() {
let mut connection = Connection::open_in_memory().unwrap();
let migrations = migrations();
migrations.to_latest(&mut connection).unwrap();
let request_count = connection
.query_row("SELECT COUNT(*) FROM requests_v2", [], |row| {
row.get::<_, u32>(0)
})
.unwrap();
assert_eq!(request_count, 0);
}
#[test]
fn test_migrate_request_body_kind() {
fn insert_exchange(
connection: &Connection,
request_body: Option<&[u8]>,
) {
connection
.execute(
"INSERT INTO
requests_v2 (
id,
collection_id,
profile_id,
recipe_id,
start_time,
end_time,
http_version,
method,
url,
request_headers,
request_body,
status_code,
response_headers,
response_body
)
VALUES (
:id,
:collection_id,
:profile_id,
:recipe_id,
:start_time,
:end_time,
:http_version,
:method,
:url,
:request_headers,
:request_body,
:status_code,
:response_headers,
:response_body
)",
named_params! {
":id": RequestId::new(),
":collection_id": CollectionId::new(),
":profile_id": "",
":recipe_id": RecipeId::factory(()),
":start_time": Utc::now(),
":end_time": Utc::now(),
":http_version": "1.1",
":method": "POST",
":url": "http://localhost",
":request_headers": b"",
":request_body": request_body,
":status_code": 200,
":response_headers": b"",
":response_body": b"",
},
)
.unwrap();
}
let mut connection = Connection::open_in_memory().unwrap();
let migrations = migrations();
migrations.to_version(&mut connection, 10).unwrap();
let columns: Vec<String> = connection
.prepare("PRAGMA table_info(requests_v2)")
.unwrap()
.query_map((), |row| row.get::<_, String>("name"))
.unwrap()
.collect::<Result<Vec<String>, _>>()
.unwrap();
assert!(!columns.contains(&"request_body_kind".to_owned()));
connection
.pragma_update(None, "foreign_keys", "OFF")
.unwrap();
insert_exchange(&connection, None); insert_exchange(&connection, Some(b"")); insert_exchange(&connection, Some(b"data"));
migrations.to_version(&mut connection, 11).unwrap();
let bodies = connection
.prepare(
"SELECT request_body_kind, request_body FROM requests_v2 \
ORDER BY start_time",
)
.unwrap()
.query_map((), |row| {
let kind: u8 = row.get("request_body_kind")?;
let body: Option<Vec<u8>> = row.get("request_body")?;
Ok((kind, body))
})
.unwrap()
.collect::<Result<Vec<(u8, Option<Vec<u8>>)>, _>>()
.unwrap();
assert_eq!(
bodies,
vec![
(0, None),
(1, Some(b"".to_vec())),
(1, Some(b"data".to_vec()))
]
);
}
}