use chrono::Utc;
use serde_json::Value;
use super::super::ast::*;
use crate::error::{DbError, DbResult};
const MAX_REGEX_PATTERN_LEN: usize = 1024;
const MAX_REGEX_SIZE: usize = 1 << 20;
pub fn safe_regex(pattern: &str) -> Result<regex::Regex, DbError> {
if pattern.len() > MAX_REGEX_PATTERN_LEN {
return Err(DbError::ExecutionError(format!(
"Regex pattern too long: {} bytes (max {})",
pattern.len(),
MAX_REGEX_PATTERN_LEN
)));
}
regex::RegexBuilder::new(pattern)
.size_limit(MAX_REGEX_SIZE)
.build()
.map_err(|e| DbError::ExecutionError(format!("Invalid regex pattern: {}", e)))
}
pub fn number_from_f64(f: f64) -> serde_json::Number {
serde_json::Number::from_f64(f).unwrap_or_else(|| serde_json::Number::from(0))
}
pub const SECONDS_EPOCH_THRESHOLD: f64 = 10_000_000_000.0;
#[derive(Debug, Clone, Copy)]
pub enum ParsedDate {
Offset(chrono::DateTime<chrono::FixedOffset>),
Naive(chrono::NaiveDateTime),
}
impl ParsedDate {
pub fn to_utc(self) -> chrono::DateTime<Utc> {
match self {
ParsedDate::Offset(dt) => dt.with_timezone(&Utc),
ParsedDate::Naive(n) => n.and_utc(),
}
}
pub fn naive_local(self) -> chrono::NaiveDateTime {
match self {
ParsedDate::Offset(dt) => dt.naive_local(),
ParsedDate::Naive(n) => n,
}
}
}
pub fn parse_date_str(s: &str) -> Option<ParsedDate> {
use chrono::{DateTime, NaiveDate, NaiveDateTime};
let s = s.trim();
if s.len() < 10 || !s.is_char_boundary(10) {
return None;
}
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Some(ParsedDate::Offset(dt));
}
if s.len() == 10 {
return NaiveDate::parse_from_str(s, "%Y-%m-%d")
.ok()
.and_then(|d| d.and_hms_opt(0, 0, 0))
.map(ParsedDate::Naive);
}
let (date, rest) = s.split_at(10);
let rest = rest
.strip_prefix('T')
.or_else(|| rest.strip_prefix('t'))
.or_else(|| rest.strip_prefix(' '))?;
let off_at = rest
.char_indices()
.find(|(_, c)| !(c.is_ascii_digit() || *c == ':' || *c == '.'))
.map(|(i, _)| i)
.unwrap_or(rest.len());
let (time, offset) = rest.split_at(off_at);
let naive_str = format!("{date}T{time}");
let naive = NaiveDateTime::parse_from_str(&naive_str, "%Y-%m-%dT%H:%M:%S%.f")
.or_else(|_| NaiveDateTime::parse_from_str(&naive_str, "%Y-%m-%dT%H:%M"))
.ok()?;
let offset = offset.trim();
if offset.is_empty() {
return Some(ParsedDate::Naive(naive));
}
if offset.eq_ignore_ascii_case("z") {
return Some(ParsedDate::Offset(naive.and_utc().fixed_offset()));
}
let secs = parse_offset_secs(offset)?;
let fixed = chrono::FixedOffset::east_opt(secs)?;
naive
.and_local_timezone(fixed)
.single()
.map(ParsedDate::Offset)
}
fn parse_offset_secs(s: &str) -> Option<i32> {
let mut chars = s.chars();
let sign = match chars.next()? {
'+' => 1,
'-' | '\u{2212}' => -1,
_ => return None,
};
let digits: String = chars.filter(|c| *c != ':').collect();
if !digits.chars().all(|c| c.is_ascii_digit()) {
return None;
}
let (h, m) = match digits.len() {
2 => (digits.parse::<i32>().ok()?, 0),
4 => (
digits[..2].parse::<i32>().ok()?,
digits[2..].parse::<i32>().ok()?,
),
_ => return None,
};
if h > 23 || m > 59 {
return None;
}
Some(sign * (h * 3600 + m * 60))
}
pub fn number_to_epoch_ms(n: &serde_json::Number) -> DbResult<i64> {
if let Some(i) = n.as_i64() {
return Ok(if (i as f64).abs() < SECONDS_EPOCH_THRESHOLD {
i.saturating_mul(1000)
} else {
i
});
}
let f = n
.as_f64()
.filter(|f| f.is_finite())
.ok_or_else(|| DbError::ExecutionError("Invalid timestamp".to_string()))?;
let ms = if f.abs() < SECONDS_EPOCH_THRESHOLD {
f * 1000.0
} else {
f
};
if ms.abs() >= 9.2e18 {
return Err(DbError::ExecutionError(format!("Invalid timestamp: {}", f)));
}
Ok(ms.floor() as i64)
}
pub fn parse_datetime(value: &Value) -> DbResult<chrono::DateTime<Utc>> {
match value {
Value::Number(n) => {
let timestamp_ms = number_to_epoch_ms(n)?;
chrono::DateTime::from_timestamp_millis(timestamp_ms).ok_or_else(|| {
DbError::ExecutionError(format!("Invalid timestamp: {}", timestamp_ms))
})
}
Value::String(s) => parse_date_str(s)
.map(ParsedDate::to_utc)
.ok_or_else(|| DbError::ExecutionError(format!("Invalid date string '{}'", s))),
_ => Err(DbError::ExecutionError(
"Date must be a timestamp or date string".to_string(),
)),
}
}
pub fn parse_date_value(value: &Value) -> DbResult<ParsedDate> {
match value {
Value::String(s) => parse_date_str(s)
.ok_or_else(|| DbError::ExecutionError(format!("Invalid date string '{}'", s))),
other => Ok(ParsedDate::Naive(parse_datetime(other)?.naive_utc())),
}
}
pub fn format_expression(expr: &Expression) -> String {
match expr {
Expression::Variable(name) => name.clone(),
Expression::BindVariable(name) => format!("@{}", name),
Expression::FieldAccess(base, field) => {
format!("{}.{}", format_expression(base), field)
}
Expression::OptionalFieldAccess(base, field) => {
format!("{}?.{}", format_expression(base), field)
}
Expression::DynamicFieldAccess(base, field_expr) => {
format!(
"{}[{}]",
format_expression(base),
format_expression(field_expr)
)
}
Expression::ArrayAccess(base, index) => {
format!("{}[{}]", format_expression(base), format_expression(index))
}
Expression::ArraySpreadAccess(base, field_path) => {
let base_str = format_expression(base);
match field_path {
Some(path) => format!("{}[*].{}", base_str, path),
None => format!("{}[*]", base_str),
}
}
Expression::Literal(value) => format!("{}", value),
Expression::FunctionCall { name, args } => {
let args_str = args
.iter()
.map(format_expression)
.collect::<Vec<_>>()
.join(", ");
format!("{}({})", name, args_str)
}
Expression::Pipeline { left, right } => {
format!(
"{} |> {}",
format_expression(left),
format_expression(right)
)
}
Expression::Lambda { params, body } => {
if params.len() == 1 {
format!("{} -> {}", params[0], format_expression(body))
} else {
format!("({}) -> {}", params.join(", "), format_expression(body))
}
}
_ => format!("{:?}", expr), }
}