use sqlx::{PgPool, Postgres, Transaction};
pub(crate) struct SchemaCheck {
pub(crate) resolved_schema: Option<String>,
pub(crate) configured_exists: bool,
pub(crate) search_path: String,
pub(crate) satisfied_required_columns: Vec<String>,
}
pub(crate) async fn verify_table_schema(
pool: &PgPool,
schema: &str,
table: &str,
required_columns: &[&str],
) -> Result<SchemaCheck, sqlx::Error> {
let qualified = format!("{schema}.{table}");
let required: Vec<String> = required_columns.iter().map(|&c| c.to_owned()).collect();
let row = sqlx::query!(
r#"SELECT
current_setting('search_path') AS "search_path!",
(SELECT n.nspname
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.oid = to_regclass($2)) AS resolved_schema,
(to_regclass($1) IS NOT NULL) AS "configured_exists!",
ARRAY(
SELECT a.attname FROM pg_attribute a
WHERE a.attrelid = to_regclass($2) AND a.attname = ANY($3::text[])
AND NOT a.attisdropped AND a.attnum > 0 AND a.attnotnull
) AS "satisfied_required_columns!: Vec<String>""#,
qualified,
table,
&required,
)
.fetch_one(pool)
.await?;
Ok(SchemaCheck {
resolved_schema: row.resolved_schema,
configured_exists: row.configured_exists,
search_path: row.search_path,
satisfied_required_columns: row.satisfied_required_columns,
})
}
pub(crate) async fn other_table_schemas(
pool: &PgPool,
schema: &str,
table: &str,
) -> Result<Vec<String>, sqlx::Error> {
sqlx::query_scalar!(
r#"SELECT n.nspname
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = $2 AND n.nspname <> $1"#,
schema,
table,
)
.fetch_all(pool)
.await
}
pub(crate) async fn set_search_path(
tx: &mut Transaction<'_, Postgres>,
schema: &str,
) -> Result<String, sqlx::Error> {
let previous: String = sqlx::query_scalar!("SELECT current_setting('search_path')")
.fetch_one(&mut **tx)
.await?
.unwrap_or_default();
let wanted = format!("{schema},public");
sqlx::query_scalar!("SELECT set_config('search_path', $1, true)", wanted)
.fetch_one(&mut **tx)
.await?;
Ok(previous)
}
pub(crate) async fn restore_search_path(
tx: &mut Transaction<'_, Postgres>,
previous: &str,
) -> Result<(), sqlx::Error> {
sqlx::query_scalar!("SELECT set_config('search_path', $1, true)", previous)
.fetch_one(&mut **tx)
.await?;
Ok(())
}
pub(crate) fn is_valid_schema_name(schema: &str) -> bool {
let mut chars = schema.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_lowercase() || first == '_') {
return false;
}
schema.len() <= 63
&& chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '$')
}