use serde_json::{Value as Json, json};
use super::{CompileContext, CompileError};
use crate::generation::ManifestTable;
use crate::ir::operations::AggregateExpr;
use crate::ir::value::LogicalValue;
pub(super) fn value_to_json(v: &LogicalValue) -> Json {
match v {
LogicalValue::Null => Json::Null,
LogicalValue::Bool(b) => Json::Bool(*b),
LogicalValue::Int(i) => Json::Number((*i).into()),
LogicalValue::Float(f) => serde_json::Number::from_f64(*f)
.map(Json::Number)
.unwrap_or(Json::Null),
LogicalValue::String(s) => Json::String(s.clone()),
LogicalValue::Bytes(b) => {
use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
json!({ "$binary": { "base64": B64.encode(b), "subType": "00" } })
}
LogicalValue::Timestamp(t) => json!({ "$date": t.to_rfc3339() }),
LogicalValue::Json(j) => j.clone(),
LogicalValue::Array(values) => Json::Array(values.iter().map(value_to_json).collect()),
}
}
pub(super) fn like_to_regex(pattern: &str) -> String {
let mut out = String::from("^");
for ch in pattern.chars() {
match ch {
'%' => out.push_str(".*"),
'_' => out.push('.'),
'.' | '\\' | '^' | '$' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' => {
out.push('\\');
out.push(ch);
}
other => out.push(other),
}
}
out.push('$');
out
}
pub(super) fn regex_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
if matches!(
ch,
'.' | '\\' | '^' | '$' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|'
) {
out.push('\\');
}
out.push(ch);
}
out
}
pub(super) fn validate_aggregate_aliases(aggregates: &[AggregateExpr]) -> Result<(), CompileError> {
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
for agg in aggregates {
if !seen.insert(agg.alias.as_str()) {
return Err(CompileError::Malformed {
reason: format!("duplicate aggregate alias '{}'", agg.alias),
});
}
}
Ok(())
}
pub(super) fn validate_no_groupby_alias_collision(
group_column_names: &[&str],
aggregates: &[AggregateExpr],
) -> Result<(), CompileError> {
if let Some(agg) = aggregates
.iter()
.find(|a| group_column_names.iter().any(|g| *g == a.alias.as_str()))
{
return Err(CompileError::Malformed {
reason: format!(
"aggregate alias '{}' collides with a GROUP BY column of the same name; \
rename the aggregate",
agg.alias
),
});
}
Ok(())
}
pub(super) fn resolve_tenant_column(table: &ManifestTable) -> Option<&str> {
if let Some(c) = table.columns.iter().find(|c| c.is_tenant_column) {
return Some(c.column_name.as_str());
}
table
.columns
.iter()
.find(|c| {
let cn = c.column_name.as_str();
let fnm = c.field_name.as_str();
cn.eq_ignore_ascii_case("tenant_id")
|| cn.eq_ignore_ascii_case("_tenant_id")
|| fnm.eq_ignore_ascii_case("tenant_id")
|| fnm.eq_ignore_ascii_case("_tenant_id")
})
.map(|c| c.column_name.as_str())
}
pub(super) fn resolve_project_column(table: &ManifestTable) -> Option<&str> {
table
.columns
.iter()
.find(|c| {
let cn = c.column_name.as_str();
let fnm = c.field_name.as_str();
cn.eq_ignore_ascii_case("project_id")
|| cn.eq_ignore_ascii_case("_project_id")
|| fnm.eq_ignore_ascii_case("project_id")
|| fnm.eq_ignore_ascii_case("_project_id")
})
.map(|c| c.column_name.as_str())
}
pub(super) fn tenant_system_field(table: &ManifestTable) -> &str {
resolve_tenant_column(table).unwrap_or("_tenant_id")
}
pub(super) fn project_system_field(table: &ManifestTable) -> &str {
resolve_project_column(table).unwrap_or("_project_id")
}
pub(super) fn append_context_predicates(
body: Option<String>,
table: &ManifestTable,
ctx: &CompileContext<'_>,
params: &mut Vec<LogicalValue>,
quote: char,
) -> Option<String> {
let mut parts: Vec<String> = body.into_iter().collect();
if let Some(tid) = ctx.tenant_id
&& !tid.is_empty()
&& let Some(col) = resolve_tenant_column(table)
{
params.push(LogicalValue::String(tid.to_string()));
parts.push(format!("{quote}{col}{quote} = ?"));
}
if let Some(pid) = ctx.project_id
&& !pid.is_empty()
&& let Some(col) = resolve_project_column(table)
{
params.push(LogicalValue::String(pid.to_string()));
parts.push(format!("{quote}{col}{quote} = ?"));
}
if parts.is_empty() {
None
} else if parts.len() == 1 {
parts.pop()
} else {
Some(parts.join(" AND "))
}
}