use sqlx::postgres::types::Oid;
use tracing::{debug, trace};
use super::oid_index::OidIndex;
const SYSTEM_SCHEMAS: [&str; 3] = ["pg_catalog", "information_schema", "pg_toast"];
pub fn is_system_schema(schema: &str) -> bool {
SYSTEM_SCHEMAS.contains(&schema)
|| schema.starts_with("pg_temp_")
|| schema.starts_with("pg_toast_temp_")
}
pub const BUILT_IN_EXTENSIONS: [&str; 1] = ["plpgsql"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExclusionReason {
SystemSchema,
ExtensionOwned { extension: String },
ConstraintBackingIndex { constraint: String },
InternalTrigger,
BuiltInExtension,
IdentityOwnedSequence { table: String, column: String },
}
impl ExclusionReason {
#[allow(dead_code)]
pub fn name(&self) -> &'static str {
match self {
ExclusionReason::SystemSchema => "SystemSchema",
ExclusionReason::ExtensionOwned { .. } => "ExtensionOwned",
ExclusionReason::ConstraintBackingIndex { .. } => "ConstraintBackingIndex",
ExclusionReason::InternalTrigger => "InternalTrigger",
ExclusionReason::BuiltInExtension => "BuiltInExtension",
ExclusionReason::IdentityOwnedSequence { .. } => "IdentityOwnedSequence",
}
}
}
#[derive(Debug, Clone)]
pub struct Excluded {
#[allow(dead_code)]
pub oid: Oid,
#[allow(dead_code)]
pub kind: &'static str,
pub schema: String,
pub name: String,
pub reason: ExclusionReason,
}
impl Excluded {
pub fn new(
oid: Oid,
kind: &'static str,
schema: &str,
name: &str,
reason: ExclusionReason,
) -> Self {
Self {
oid,
kind,
schema: schema.to_string(),
name: name.to_string(),
reason,
}
}
pub fn qualified_name(&self) -> String {
format!("{}.{}", self.schema, self.name)
}
}
#[derive(Debug, Clone)]
pub struct Converted<T> {
pub objects: Vec<T>,
pub excluded: Vec<Excluded>,
pub index: OidIndex,
}
impl<T> Converted<T> {
pub fn new() -> Self {
Self {
objects: Vec::new(),
excluded: Vec::new(),
index: OidIndex::new(),
}
}
pub fn map<U>(self, f: impl FnMut(T) -> U) -> Converted<U> {
Converted {
objects: self.objects.into_iter().map(f).collect(),
excluded: self.excluded,
index: self.index,
}
}
pub fn log_and_take_objects(self, kind: &str) -> Vec<T> {
self.log(kind);
self.objects
}
pub fn collect_into(
mut self,
kind: &'static str,
indexes: &mut Vec<(&'static str, OidIndex)>,
) -> Vec<T> {
self.log(kind);
indexes.push((kind, std::mem::take(&mut self.index)));
self.objects
}
fn log(&self, kind: &str) {
debug!(
"Converted {} {kind} rows, excluded {}",
self.objects.len(),
self.excluded.len()
);
for row in &self.excluded {
trace!("Excluded {kind} {}: {:?}", row.qualified_name(), row.reason);
}
}
#[allow(dead_code)]
pub fn excluded_for(&self, reason: &str) -> impl Iterator<Item = &Excluded> {
self.excluded
.iter()
.filter(move |row| row.reason.name() == reason)
}
}
impl<T> Default for Converted<T> {
fn default() -> Self {
Self::new()
}
}
pub mod sql {
use super::{BUILT_IN_EXTENSIONS, SYSTEM_SCHEMAS};
fn system_schema_list() -> String {
let names: Vec<String> = SYSTEM_SCHEMAS
.iter()
.map(|name| format!("'{name}'"))
.collect();
format!("({})", names.join(", "))
}
pub fn not_a_system_namespace(nspname_expr: &str) -> String {
format!(
"{nspname_expr} NOT IN {} AND {nspname_expr} NOT LIKE 'pg_temp_%' AND {nspname_expr} NOT LIKE 'pg_toast_temp_%'",
system_schema_list()
)
}
pub fn not_extension_owned(class: &str, oid_expr: &str) -> String {
format!(
"NOT EXISTS (\n SELECT 1 FROM pg_depend dep\n \
WHERE dep.objid = {oid_expr}\n \
AND dep.classid = '{class}'::regclass\n \
AND dep.refclassid = 'pg_extension'::regclass\n \
AND dep.deptype = 'e'\n)"
)
}
pub fn parent_relation_not_extension_owned(parent_oid_expr: &str) -> String {
not_extension_owned("pg_class", parent_oid_expr)
}
pub fn not_a_constraint_backing_index(index_oid_expr: &str) -> String {
format!(
"NOT EXISTS (\n SELECT 1 FROM pg_constraint con\n \
WHERE con.conindid = {index_oid_expr}\n \
AND con.contype IN ('p', 'u', 'x')\n)"
)
}
pub fn not_an_identity_sequence(sequence_oid_expr: &str) -> String {
format!(
"NOT EXISTS (\n SELECT 1 FROM pg_depend dep\n \
WHERE dep.objid = {sequence_oid_expr}\n \
AND dep.classid = 'pg_class'::regclass\n \
AND dep.deptype = 'i'\n)"
)
}
pub fn not_a_built_in_extension(extname_expr: &str) -> String {
let names: Vec<String> = BUILT_IN_EXTENSIONS
.iter()
.map(|name| format!("'{name}'"))
.collect();
format!("{extname_expr} NOT IN ({})", names.join(", "))
}
#[cfg(test)]
mod tests {
use super::not_a_system_namespace;
#[test]
fn test_system_namespace_predicate_is_stable() {
assert_eq!(
not_a_system_namespace("n.nspname"),
"n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') \
AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%'"
);
}
}
}
#[cfg(test)]
mod tests {
use super::is_system_schema;
#[test]
fn test_temporary_namespaces_belong_to_postgres() {
assert!(is_system_schema("pg_catalog"));
assert!(is_system_schema("information_schema"));
assert!(is_system_schema("pg_toast"));
assert!(is_system_schema("pg_temp_3"));
assert!(is_system_schema("pg_toast_temp_3"));
assert!(!is_system_schema("public"));
assert!(!is_system_schema("app"));
assert!(!is_system_schema("pg_temporary_data"));
}
}