use anyhow::{Context, Result};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteQueryResult, SqliteRow};
use sqlx::{AssertSqlSafe, Row, TypeInfo, ValueRef};
use super::query::{self, Cell};
use super::{ConnectionConfig, decode, pool_options, quote_literal};
pub(crate) const DATABASES_SQL: &str = "SELECT name FROM pragma_database_list ORDER BY seq";
pub(crate) const OBJECTS_SQL: &str = "SELECT NULL AS table_schema, name, \
CASE type WHEN 'view' THEN 'VIEW' ELSE 'BASE TABLE' END AS table_type \
FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' \
ORDER BY name";
pub(crate) const CATALOG_COLUMNS_SQL: &str = "SELECT NULL, m.name, m.type, ti.name, ti.type \
FROM sqlite_master m JOIN pragma_table_info(m.name) ti \
WHERE m.type IN ('table', 'view') AND m.name NOT LIKE 'sqlite_%' \
ORDER BY m.name, ti.cid";
pub(crate) const CATALOG_INDEXES_SQL: &str = "SELECT NULL, m.name, m.type, il.name, \
(SELECT group_concat(ii.name, ', ') FROM pragma_index_info(il.name) ii) \
FROM sqlite_master m JOIN pragma_index_list(m.name) il \
WHERE m.type IN ('table', 'view') AND m.name NOT LIKE 'sqlite_%' \
AND il.origin = 'c' \
ORDER BY m.name, il.name";
pub(crate) const CATALOG_TRIGGERS_SQL: &str = "SELECT NULL, tr.tbl_name, COALESCE(o.type, 'table'), tr.name, '' \
FROM sqlite_master tr \
LEFT JOIN sqlite_master o ON o.name = tr.tbl_name AND o.type IN ('table', 'view') \
WHERE tr.type = 'trigger' \
ORDER BY tr.tbl_name, tr.name";
pub(crate) async fn connect(config: &ConnectionConfig) -> Result<SqlitePool> {
if config.database.trim().is_empty() {
anyhow::bail!("no database file selected");
}
let options = SqliteConnectOptions::new()
.filename(&config.database)
.create_if_missing(false)
.read_only(config.safety.is_read_only());
let pool = pool_options()
.connect_with(options)
.await
.with_context(|| format!("could not open {}", config.database))?;
Ok(pool)
}
pub(crate) fn primary_key_sql(table: &str) -> String {
format!(
"SELECT name FROM pragma_table_info({}) WHERE pk > 0 ORDER BY pk",
quote_literal(table)
)
}
pub(crate) fn columns_sql(table: &str) -> String {
format!(
"SELECT name, type, (\"notnull\" = 0), dflt_value \
FROM pragma_table_info({}) ORDER BY cid",
quote_literal(table)
)
}
pub(crate) fn indexes_sql(table: &str) -> String {
format!(
"SELECT il.name, \
(SELECT group_concat(ii.name, ',') FROM pragma_index_info(il.name) ii), \
il.\"unique\", (il.origin = 'pk') \
FROM pragma_index_list({}) il \
ORDER BY il.seq",
quote_literal(table)
)
}
pub(crate) fn foreign_keys_sql(table: &str) -> String {
format!(
"SELECT NULL, group_concat(\"from\", ','), NULL, \"table\", \
group_concat(\"to\", ','), on_delete, on_update \
FROM pragma_foreign_key_list({}) \
GROUP BY id ORDER BY id",
quote_literal(table)
)
}
pub(crate) fn rows_affected(result: &SqliteQueryResult) -> u64 {
result.rows_affected()
}
pub(crate) fn master_sql(table: &str) -> String {
format!(
"SELECT type, name, sql FROM sqlite_master \
WHERE sql IS NOT NULL AND (tbl_name = {} OR type = 'view') \
ORDER BY type, name",
quote_literal(table)
)
}
pub(crate) async fn rebuild(pool: &SqlitePool, statements: &[String]) -> Result<()> {
let mut connection = pool
.acquire()
.await
.context("could not take a connection for the table rebuild")?;
connection.close_on_drop();
let foreign_keys: bool = sqlx::query_scalar::<_, i64>("PRAGMA foreign_keys")
.fetch_one(&mut *connection)
.await
.map(|value| value != 0)
.unwrap_or(false);
if foreign_keys {
sqlx::query("PRAGMA foreign_keys = OFF")
.execute(&mut *connection)
.await
.context("could not turn foreign keys off for the table rebuild")?;
}
sqlx::query("PRAGMA legacy_alter_table = ON")
.execute(&mut *connection)
.await
.context("could not prepare the table rebuild")?;
let mut transaction = sqlx::Connection::begin(&mut *connection)
.await
.context("could not start the table rebuild's transaction")?;
for (position, statement) in statements.iter().enumerate() {
sqlx::query(AssertSqlSafe(statement.clone()))
.execute(&mut *transaction)
.await
.with_context(|| format!("statement {} of {}", position + 1, statements.len()))?;
}
let violations = sqlx::query("PRAGMA foreign_key_check")
.fetch_all(&mut *transaction)
.await
.context("could not check foreign keys after the table rebuild")?;
if !violations.is_empty() {
transaction.rollback().await.ok();
anyhow::bail!(
"the rebuild would leave {} foreign key relationship{} broken, so it was rolled back",
violations.len(),
if violations.len() == 1 { "" } else { "s" }
);
}
transaction
.commit()
.await
.context("could not commit the table rebuild")?;
if foreign_keys {
sqlx::query("PRAGMA foreign_keys = ON")
.execute(&mut *connection)
.await
.ok();
}
Ok(())
}
pub(crate) fn cell(row: &SqliteRow, index: usize) -> Cell {
let Ok(raw) = row.try_get_raw(index) else {
return None;
};
if raw.is_null() {
return None;
}
let type_name = raw.type_info().name().to_string();
let value = match type_name.as_str() {
"INTEGER" => decode!(row, index, i64),
"REAL" => decode!(row, index, f64),
"BOOLEAN" => row.try_get::<bool, _>(index).ok().map(query::boolean),
"BLOB" => row
.try_get::<Vec<u8>, _>(index)
.ok()
.and_then(|bytes| query::blob(&bytes)),
_ => decode!(row, index, String),
};
value.or_else(|| query::unsupported(&type_name))
}
pub(crate) fn raw_bytes(row: &SqliteRow, index: usize) -> Option<Vec<u8>> {
row.try_get::<Option<Vec<u8>>, _>(index).ok().flatten()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Maintenance {
IntegrityCheck,
QuickCheck,
ForeignKeyCheck,
Optimize,
Analyze,
Vacuum,
WalCheckpoint,
}
impl Maintenance {
pub const ALL: [Maintenance; 7] = [
Maintenance::IntegrityCheck,
Maintenance::QuickCheck,
Maintenance::ForeignKeyCheck,
Maintenance::Optimize,
Maintenance::Analyze,
Maintenance::Vacuum,
Maintenance::WalCheckpoint,
];
pub fn label(self) -> &'static str {
match self {
Maintenance::IntegrityCheck => "Integrity Check",
Maintenance::QuickCheck => "Quick Check",
Maintenance::ForeignKeyCheck => "Foreign Key Check",
Maintenance::Optimize => "Optimize",
Maintenance::Analyze => "Analyze",
Maintenance::Vacuum => "Vacuum",
Maintenance::WalCheckpoint => "WAL Checkpoint",
}
}
pub fn description(self) -> &'static str {
match self {
Maintenance::IntegrityCheck => {
"Read the whole file and report corruption; `ok` means none found"
}
Maintenance::QuickCheck => {
"Like the integrity check, but skips verifying index contents"
}
Maintenance::ForeignKeyCheck => "List rows whose foreign key points at nothing",
Maintenance::Optimize => "Refresh query-planner statistics where they have gone stale",
Maintenance::Analyze => "Rebuild query-planner statistics for every table",
Maintenance::Vacuum => {
"Rewrite the file to reclaim free pages; needs free disk space about the size of the database"
}
Maintenance::WalCheckpoint => {
"Move the write-ahead log into the database file and empty it"
}
}
}
pub fn sql(self) -> &'static str {
match self {
Maintenance::IntegrityCheck => "PRAGMA integrity_check",
Maintenance::QuickCheck => "PRAGMA quick_check",
Maintenance::ForeignKeyCheck => "PRAGMA foreign_key_check",
Maintenance::Optimize => "PRAGMA optimize",
Maintenance::Analyze => "ANALYZE",
Maintenance::Vacuum => "VACUUM",
Maintenance::WalCheckpoint => "PRAGMA wal_checkpoint(TRUNCATE)",
}
}
pub fn writes(self) -> bool {
match self {
Maintenance::IntegrityCheck
| Maintenance::QuickCheck
| Maintenance::ForeignKeyCheck => false,
Maintenance::Optimize
| Maintenance::Analyze
| Maintenance::Vacuum
| Maintenance::WalCheckpoint => true,
}
}
}
#[cfg(test)]
mod maintenance_tests {
use super::Maintenance;
#[test]
fn every_task_has_one_statement() {
for task in Maintenance::ALL {
assert!(!task.sql().contains(';'), "{task:?}");
assert!(!task.label().is_empty() && !task.description().is_empty());
}
}
#[test]
fn only_the_checks_are_reads() {
let reads: Vec<_> = Maintenance::ALL
.into_iter()
.filter(|task| !task.writes())
.collect();
assert_eq!(
reads,
[
Maintenance::IntegrityCheck,
Maintenance::QuickCheck,
Maintenance::ForeignKeyCheck
]
);
}
}