use serde_json::Value;
use super::{CostWarning, DesignAudit, IssueSeverity};
pub fn analyze(schema: &Value, audit: &mut DesignAudit) {
check_worst_case_complexity(schema, audit);
check_unbounded_pagination(schema, audit);
check_field_multipliers(schema, audit);
}
fn check_worst_case_complexity(schema: &Value, audit: &mut DesignAudit) {
if let Some(types) = schema.get("types").and_then(|v| v.as_array()) {
let mut type_multipliers: std::collections::HashMap<String, u32> =
std::collections::HashMap::new();
for type_def in types {
let type_name =
type_def.get("name").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
if let Some(fields) = type_def.get("fields").and_then(|v| v.as_array()) {
for field in fields {
if let Some(multiplier) =
field.get("complexity_multiplier").and_then(|v| v.as_u64())
{
let current = type_multipliers.entry(type_name.clone()).or_insert(0);
*current = (u64::from(*current) + multiplier).min(10000) as u32;
}
}
}
}
for type_def in types {
let type_name = type_def.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
if let Some(fields) = type_def.get("fields").and_then(|v| v.as_array()) {
for field in fields {
if let Some(field_type) = field.get("type").and_then(|v| v.as_str()) {
if field_type.contains('[') && field_type.contains(']') {
let inner_type = field_type
.trim_start_matches('[')
.trim_end_matches(']')
.trim_matches('!')
.to_string();
let base_multiplier = u32::try_from(
field
.get("complexity_multiplier")
.and_then(|v| v.as_u64())
.unwrap_or(0),
)
.unwrap_or(u32::MAX);
let inner_multiplier =
type_multipliers.get(&inner_type).copied().unwrap_or(0);
let compound = u32::try_from(
(u64::from(base_multiplier) * u64::from(inner_multiplier))
.min(10000),
)
.unwrap_or(10000);
if compound > 1000 || base_multiplier > 50 {
let severity = if compound > 5000 {
IssueSeverity::Critical
} else {
IssueSeverity::Warning
};
let max_complexity = compound.max(base_multiplier);
audit.cost_warnings.push(CostWarning {
severity,
message: format!(
"Compiled JSONB cost: {}.{} can reach {} cardinality - May not compile to deterministic SQL",
type_name,
field.get("name").and_then(|v| v.as_str()).unwrap_or("unknown"),
max_complexity
),
suggestion: "Reduce JSONB nesting depth or add pagination limits so compiler can guarantee constant-time execution".to_string(),
worst_case_complexity: Some(max_complexity),
});
}
}
}
}
}
}
}
}
fn check_unbounded_pagination(schema: &Value, audit: &mut DesignAudit) {
if let Some(types) = schema.get("types").and_then(|v| v.as_array()) {
for type_def in types {
if let Some(fields) = type_def.get("fields").and_then(|v| v.as_array()) {
for field in fields {
if let Some(field_type) = field.get("type").and_then(|v| v.as_str()) {
if field_type.contains('[') && field_type.contains(']') {
let has_default_limit =
field.get("default_limit").is_some_and(|v| !v.is_null());
if !has_default_limit {
let field_name =
field.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
audit.cost_warnings.push(CostWarning {
severity: IssueSeverity::Warning,
message: format!(
"Unbounded pagination on {}: No default limit - Compiler can't pre-compute worst-case JSONB cardinality",
field_name
),
suggestion: "Add defaultLimit so compiler can guarantee deterministic query cost at compile time".to_string(),
worst_case_complexity: None,
});
}
}
}
}
}
}
}
}
fn check_field_multipliers(schema: &Value, audit: &mut DesignAudit) {
if let Some(types) = schema.get("types").and_then(|v| v.as_array()) {
let mut type_map: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for type_def in types {
let type_name =
type_def.get("name").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
if let Some(fields) = type_def.get("fields").and_then(|v| v.as_array()) {
for field in fields {
if let Some(field_type) = field.get("type").and_then(|v| v.as_str()) {
if field_type.contains('[') {
type_map
.entry(type_name.clone())
.or_default()
.push(field_type.to_string());
}
}
}
}
}
for type_def in types {
let type_name = type_def.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
if let Some(fields) = type_def.get("fields").and_then(|v| v.as_array()) {
for field in fields {
if let Some(field_type) = field.get("type").and_then(|v| v.as_str()) {
if field_type.contains('[') {
let inner_type = field_type
.trim_matches('[')
.trim_matches(']')
.trim_matches('!')
.to_string();
if let Some(nested_lists) = type_map.get(&inner_type) {
if !nested_lists.is_empty() {
audit.cost_warnings.push(CostWarning {
severity: IssueSeverity::Warning,
message: format!(
"JSONB multiplier chain: {}.{} lists {} which has {} nested lists - O(n²) cardinality",
type_name,
field.get("name").and_then(|v| v.as_str()).unwrap_or("unknown"),
inner_type,
nested_lists.len()
),
suggestion: "Limit pagination on both levels to keep JSONB cardinality bounded. E.g., paginate inner lists separately.".to_string(),
worst_case_complexity: None,
});
}
}
}
}
}
}
}
}
}