use anyhow::Result;
use sqlx::PgPool;
use std::collections::BTreeSet;
use crate::catalog::id::DbObjectId;
use crate::catalog::raw::snapshot;
#[derive(sqlx::FromRow)]
struct IdentityRow {
#[sqlx(rename = "type")]
kind: String,
schema: Option<String>,
name: String,
#[sqlx(rename = "tbl")]
table: Option<String>,
args: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CatalogIdentity {
pub objects: BTreeSet<DbObjectId>,
}
impl CatalogIdentity {
pub async fn load(pool: &PgPool) -> Result<Self> {
let rows: Vec<IdentityRow> = sqlx::query_as(sqlx::AssertSqlSafe(snapshot::query()))
.fetch_all(pool)
.await?;
let mut objects = BTreeSet::new();
for row in rows {
let object_id = match row.kind.as_str() {
"schema" => DbObjectId::Schema {
name: row.name.clone(),
},
"table" => DbObjectId::Table {
schema: row.schema.clone().unwrap_or_default(),
name: row.name.clone(),
},
"view" => DbObjectId::View {
schema: row.schema.clone().unwrap_or_default(),
name: row.name.clone(),
},
"sequence" => DbObjectId::Sequence {
schema: row.schema.clone().unwrap_or_default(),
name: row.name.clone(),
},
"index" => DbObjectId::Index {
schema: row.schema.clone().unwrap_or_default(),
name: row.name.clone(),
},
"function" => DbObjectId::Function {
schema: row.schema.clone().unwrap_or_default(),
name: row.name.clone(),
arguments: row.args.clone().unwrap_or_default(),
},
"procedure" => DbObjectId::Procedure {
schema: row.schema.clone().unwrap_or_default(),
name: row.name.clone(),
arguments: row.args.clone().unwrap_or_default(),
},
"aggregate" => DbObjectId::Aggregate {
schema: row.schema.clone().unwrap_or_default(),
name: row.name.clone(),
arguments: row.args.clone().unwrap_or_default(),
},
"type" => DbObjectId::Type {
schema: row.schema.clone().unwrap_or_default(),
name: row.name.clone(),
},
"domain" => DbObjectId::Domain {
schema: row.schema.clone().unwrap_or_default(),
name: row.name.clone(),
},
"constraint" => DbObjectId::Constraint {
schema: row.schema.clone().unwrap_or_default(),
table: row.table.clone().unwrap_or_default(),
name: row.name.clone(),
},
"trigger" => DbObjectId::Trigger {
schema: row.schema.clone().unwrap_or_default(),
table: row.table.clone().unwrap_or_default(),
name: row.name.clone(),
},
"policy" => DbObjectId::Policy {
schema: row.schema.clone().unwrap_or_default(),
table: row.table.clone().unwrap_or_default(),
name: row.name.clone(),
},
"operator" => DbObjectId::Operator {
schema: row.schema.clone().unwrap_or_default(),
name: row.name.clone(),
arguments: row.args.clone().unwrap_or_default(),
},
"cast" => DbObjectId::Cast {
source: row.name.clone(),
target: row.table.clone().unwrap_or_default(),
},
"extension" => DbObjectId::Extension {
name: row.name.clone(),
},
other => {
tracing::warn!("Unknown object type in identity query: {}", other);
continue;
}
};
objects.insert(object_id);
}
Ok(Self { objects })
}
}
pub fn find_new_objects(old: &CatalogIdentity, new: &CatalogIdentity) -> Vec<DbObjectId> {
new.objects.difference(&old.objects).cloned().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_new_objects_empty() {
let old = CatalogIdentity {
objects: BTreeSet::new(),
};
let new = CatalogIdentity {
objects: BTreeSet::new(),
};
let result = find_new_objects(&old, &new);
assert!(result.is_empty());
}
#[test]
fn test_find_new_objects_detects_additions() {
let old = CatalogIdentity {
objects: BTreeSet::from([DbObjectId::Schema {
name: "existing".to_string(),
}]),
};
let new = CatalogIdentity {
objects: BTreeSet::from([
DbObjectId::Schema {
name: "existing".to_string(),
},
DbObjectId::Table {
schema: "existing".to_string(),
name: "new_table".to_string(),
},
]),
};
let result = find_new_objects(&old, &new);
assert_eq!(result.len(), 1);
assert_eq!(
result[0],
DbObjectId::Table {
schema: "existing".to_string(),
name: "new_table".to_string(),
}
);
}
}