use std::sync::Arc;
use pg_query::NodeEnum;
use pg_query::protobuf::AlterTableType;
use systemprompt_extension::Extension;
use systemprompt_extension::cost::{self, CostDirective};
pub const HOT_TABLES: &[&str] = &[
"ai_requests",
"ai_request_messages",
"ai_request_payloads",
"ai_request_client_evidence",
"ai_request_tool_calls",
"analytics_events",
"event_outbox",
"logs",
"user_sessions",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExpensiveStatement {
pub position: usize,
pub table: String,
pub form: &'static str,
}
#[derive(Debug, Clone)]
pub struct MigrationCost {
pub extension: String,
pub migration: String,
pub statements: Vec<ExpensiveStatement>,
pub declared: Option<CostDirective>,
pub malformed: Option<String>,
}
impl MigrationCost {
#[must_use]
pub const fn is_undeclared(&self) -> bool {
!self.statements.is_empty() && self.declared.is_none()
}
#[must_use]
pub fn label(&self) -> String {
format!("{}/{}", self.extension, self.migration)
}
#[must_use]
pub fn statement_summary(&self) -> String {
self.statements
.iter()
.map(|s| format!("statement {} {} {}", s.position, s.form, s.table))
.collect::<Vec<_>>()
.join("; ")
}
}
#[must_use]
pub fn audit_migration_cost(extensions: &[Arc<dyn Extension>], hot: &[&str]) -> Vec<MigrationCost> {
let mut out = Vec::new();
for ext in extensions {
let extension = ext.id().to_owned();
for migration in ext.migrations().into_iter().filter(|m| !m.tombstone) {
let label = format!("{:03}_{}", migration.version, migration.name);
if let Some(cost) = audit_one(&extension, &label, migration.sql, hot) {
out.push(cost);
}
}
}
out
}
#[must_use]
pub fn audit_one(
extension: &str,
migration: &str,
sql: &str,
hot: &[&str],
) -> Option<MigrationCost> {
let (declared, malformed) = match cost::parse(sql) {
Ok(found) => (found, None),
Err(e) => (None, Some(e.to_string())),
};
let statements = expensive_statements(sql, hot);
if statements.is_empty() && declared.is_none() && malformed.is_none() {
return None;
}
Some(MigrationCost {
extension: extension.to_owned(),
migration: migration.to_owned(),
statements,
declared,
malformed,
})
}
fn expensive_statements(sql: &str, hot: &[&str]) -> Vec<ExpensiveStatement> {
let Ok(parsed) = pg_query::parse(sql) else {
return Vec::new();
};
let mut out = Vec::new();
for (index, node) in parsed
.protobuf
.stmts
.iter()
.filter_map(|raw| raw.stmt.as_ref().and_then(|s| s.node.as_ref()))
.enumerate()
{
let position = index + 1;
if let Some((table, form)) = classify(node)
&& hot.contains(&table.as_str())
{
out.push(ExpensiveStatement {
position,
table,
form,
});
}
}
out
}
fn is_select_driven(select: Option<&pg_query::protobuf::Node>) -> bool {
let Some(NodeEnum::SelectStmt(select)) = select.and_then(|n| n.node.as_ref()) else {
return false;
};
select.values_lists.is_empty()
}
fn classify(node: &NodeEnum) -> Option<(String, &'static str)> {
match node {
NodeEnum::UpdateStmt(stmt) => Some((stmt.relation.as_ref()?.relname.clone(), "UPDATE on")),
NodeEnum::DeleteStmt(stmt) => {
Some((stmt.relation.as_ref()?.relname.clone(), "DELETE from"))
},
NodeEnum::InsertStmt(stmt) if is_select_driven(stmt.select_stmt.as_deref()) => Some((
stmt.relation.as_ref()?.relname.clone(),
"INSERT … SELECT into",
)),
NodeEnum::IndexStmt(stmt) if !stmt.concurrent => Some((
stmt.relation.as_ref()?.relname.clone(),
"CREATE INDEX (not CONCURRENTLY) on",
)),
NodeEnum::AlterTableStmt(stmt) => {
let table = stmt.relation.as_ref()?.relname.clone();
let form = stmt.cmds.iter().find_map(|cmd| match cmd.node.as_ref() {
Some(NodeEnum::AlterTableCmd(c)) => scanning_alter(c.subtype),
_ => None,
})?;
Some((table, form))
},
_ => None,
}
}
const fn scanning_alter(subtype: i32) -> Option<&'static str> {
if subtype == AlterTableType::AtValidateConstraint as i32 {
return Some("ALTER TABLE … VALIDATE CONSTRAINT on");
}
if subtype == AlterTableType::AtSetNotNull as i32 {
return Some("ALTER TABLE … SET NOT NULL on");
}
None
}