use std::io;
use sqlx::{AnyConnection, AnyPool, Executor};
use crate::Backend;
use crate::log::database;
const SESSIONS_SQLITE: &str = "CREATE TABLE IF NOT EXISTS turbojet_sessions (
id INTEGER PRIMARY KEY,
begin_string TEXT NOT NULL,
sender_comp_id TEXT NOT NULL,
target_comp_id TEXT NOT NULL,
extra TEXT NOT NULL DEFAULT '',
next_outgoing BIGINT NOT NULL DEFAULT 1,
next_incoming BIGINT NOT NULL DEFAULT 1,
in_flight BIGINT,
created_at TEXT,
evicted_through BIGINT,
stored_bytes BIGINT NOT NULL DEFAULT 0,
lease_holder TEXT,
lease_token TEXT,
lease_until BIGINT,
CONSTRAINT turbojet_sessions_key UNIQUE (begin_string, sender_comp_id, target_comp_id, extra)
)";
const SESSIONS_POSTGRES: &str = "CREATE TABLE IF NOT EXISTS turbojet_sessions (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
begin_string TEXT NOT NULL,
sender_comp_id TEXT NOT NULL,
target_comp_id TEXT NOT NULL,
extra TEXT NOT NULL DEFAULT '',
next_outgoing BIGINT NOT NULL DEFAULT 1,
next_incoming BIGINT NOT NULL DEFAULT 1,
in_flight BIGINT,
created_at TEXT,
evicted_through BIGINT,
stored_bytes BIGINT NOT NULL DEFAULT 0,
lease_holder TEXT,
lease_token TEXT,
lease_until BIGINT,
CONSTRAINT turbojet_sessions_key UNIQUE (begin_string, sender_comp_id, target_comp_id, extra)
)";
const MESSAGES_SQLITE: &str = "CREATE TABLE IF NOT EXISTS turbojet_messages (
session BIGINT NOT NULL REFERENCES turbojet_sessions (id),
seq BIGINT NOT NULL,
size BIGINT NOT NULL,
body BLOB NOT NULL,
PRIMARY KEY (session, seq)
) WITHOUT ROWID";
const MESSAGES_POSTGRES: &str = "CREATE TABLE IF NOT EXISTS turbojet_messages (
session BIGINT NOT NULL REFERENCES turbojet_sessions (id),
seq BIGINT NOT NULL,
size BIGINT NOT NULL,
body BYTEA NOT NULL,
PRIMARY KEY (session, seq)
)";
pub async fn migrate(pool: &AnyPool, backend: Backend) -> io::Result<()> {
let (sessions, messages) = match backend {
Backend::Sqlite => (SESSIONS_SQLITE, MESSAGES_SQLITE),
Backend::Postgres => (SESSIONS_POSTGRES, MESSAGES_POSTGRES),
};
let mut tx = pool.begin().await.map_err(database("starting the migration"))?;
tx.execute(sessions).await.map_err(database("creating turbojet_sessions"))?;
tx.execute(messages).await.map_err(database("creating turbojet_messages"))?;
tx.commit().await.map_err(database("committing the migration"))?;
match backend {
Backend::Sqlite => add_extra_sqlite(pool).await,
Backend::Postgres => add_extra_postgres(pool).await,
}
}
async fn has_extra(connection: &mut AnyConnection, backend: Backend) -> io::Result<bool> {
let query = match backend {
Backend::Sqlite => "SELECT COUNT(*) FROM pragma_table_info('turbojet_sessions') WHERE name = 'extra'",
Backend::Postgres => {
"SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = 'turbojet_sessions' AND column_name = 'extra'"
}
};
let count: i64 = sqlx::query_scalar(query)
.fetch_one(&mut *connection)
.await
.map_err(database("looking for turbojet_sessions.extra"))?;
Ok(count > 0)
}
async fn add_extra_sqlite(pool: &AnyPool) -> io::Result<()> {
let mut connection = pool.acquire().await.map_err(database("starting the migration"))?;
if has_extra(&mut connection, Backend::Sqlite).await? {
return Ok(());
}
connection.execute("PRAGMA foreign_keys = OFF").await.map_err(database("turning foreign keys off"))?;
let rebuilt = rebuild_sqlite(&mut connection).await;
if rebuilt.is_err() {
let _ = connection.execute("ROLLBACK").await;
}
connection.execute("PRAGMA foreign_keys = ON").await.map_err(database("turning foreign keys on"))?;
rebuilt
}
async fn rebuild_sqlite(connection: &mut AnyConnection) -> io::Result<()> {
connection.execute("BEGIN IMMEDIATE").await.map_err(database("starting the migration"))?;
if has_extra(connection, Backend::Sqlite).await? {
return connection.execute("COMMIT").await.map(drop).map_err(database("committing the migration"));
}
let columns = "id, begin_string, sender_comp_id, target_comp_id, next_outgoing, next_incoming, in_flight, \
created_at, evicted_through, stored_bytes, lease_holder, lease_token, lease_until";
let new_table = SESSIONS_SQLITE.replace("IF NOT EXISTS turbojet_sessions", "turbojet_sessions_new");
for (step, sql) in [
("creating the new turbojet_sessions", new_table),
(
"copying turbojet_sessions",
format!("INSERT INTO turbojet_sessions_new ({columns}) SELECT {columns} FROM turbojet_sessions"),
),
("dropping the old turbojet_sessions", "DROP TABLE turbojet_sessions".into()),
("renaming the new turbojet_sessions", "ALTER TABLE turbojet_sessions_new RENAME TO turbojet_sessions".into()),
] {
connection.execute(sql.as_str()).await.map_err(database(step))?;
}
let broken: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM pragma_foreign_key_check")
.fetch_one(&mut *connection)
.await
.map_err(database("checking foreign keys"))?;
if broken > 0 {
return Err(io::Error::other(format!("migrating turbojet_sessions would break {broken} foreign keys")));
}
connection.execute("COMMIT").await.map(drop).map_err(database("committing the migration"))
}
async fn add_extra_postgres(pool: &AnyPool) -> io::Result<()> {
let mut tx = pool.begin().await.map_err(database("starting the migration"))?;
tx.execute("LOCK TABLE turbojet_sessions IN ACCESS EXCLUSIVE MODE")
.await
.map_err(database("locking turbojet_sessions"))?;
if has_extra(&mut tx, Backend::Postgres).await? {
return tx.commit().await.map_err(database("committing the migration"));
}
let old_key: String = sqlx::query_scalar(
"SELECT conname::text FROM pg_constraint WHERE conrelid = 'turbojet_sessions'::regclass AND contype = 'u'",
)
.fetch_one(&mut *tx)
.await
.map_err(database("finding the key of turbojet_sessions"))?;
for (step, sql) in [
(
"adding turbojet_sessions.extra",
"ALTER TABLE turbojet_sessions ADD COLUMN extra TEXT NOT NULL DEFAULT ''".into(),
),
(
"dropping the old key",
format!("ALTER TABLE turbojet_sessions DROP CONSTRAINT \"{}\"", old_key.replace('"', "\"\"")),
),
(
"adding the new key",
"ALTER TABLE turbojet_sessions ADD CONSTRAINT turbojet_sessions_key \
UNIQUE (begin_string, sender_comp_id, target_comp_id, extra)"
.into(),
),
] {
tx.execute(sql.as_str()).await.map_err(database(step))?;
}
tx.commit().await.map_err(database("committing the migration"))
}
pub async fn configure_sqlite(connection: &mut AnyConnection, sync: bool) -> Result<(), sqlx::Error> {
connection.execute("PRAGMA journal_mode = WAL").await?;
connection.execute(if sync { "PRAGMA synchronous = FULL" } else { "PRAGMA synchronous = NORMAL" }).await?;
connection.execute(if sync { "PRAGMA fullfsync = ON" } else { "PRAGMA fullfsync = OFF" }).await?;
connection.execute("PRAGMA busy_timeout = 5000").await?;
Ok(())
}