feat(plugin): timeseries 聚合 API — date_trunc 时间序列
This commit is contained in:
@@ -783,6 +783,61 @@ impl DynamicTableManager {
|
||||
|
||||
Ok((sql, values))
|
||||
}
|
||||
|
||||
/// 构建时间序列查询 SQL
|
||||
pub fn build_timeseries_sql(
|
||||
table_name: &str,
|
||||
tenant_id: Uuid,
|
||||
time_field: &str,
|
||||
time_grain: &str,
|
||||
start: Option<&str>,
|
||||
end: Option<&str>,
|
||||
) -> Result<(String, Vec<Value>), String> {
|
||||
let clean_field = sanitize_identifier(time_field);
|
||||
let grain = match time_grain {
|
||||
"day" | "week" | "month" => time_grain,
|
||||
_ => return Err(format!("不支持的 time_grain: {}", time_grain)),
|
||||
};
|
||||
|
||||
let mut conditions = vec![
|
||||
"\"tenant_id\" = $1".to_string(),
|
||||
"\"deleted_at\" IS NULL".to_string(),
|
||||
];
|
||||
let mut values: Vec<Value> = vec![tenant_id.into()];
|
||||
let mut param_idx = 2;
|
||||
|
||||
if let Some(s) = start {
|
||||
conditions.push(format!(
|
||||
"(data->>'{}')::timestamp >= ${}",
|
||||
clean_field, param_idx
|
||||
));
|
||||
values.push(Value::String(Some(Box::new(s.to_string()))));
|
||||
param_idx += 1;
|
||||
}
|
||||
if let Some(e) = end {
|
||||
conditions.push(format!(
|
||||
"(data->>'{}')::timestamp < ${}",
|
||||
clean_field, param_idx
|
||||
));
|
||||
values.push(Value::String(Some(Box::new(e.to_string()))));
|
||||
}
|
||||
|
||||
let sql = format!(
|
||||
"SELECT to_char(date_trunc('{}', (data->>'{}')::timestamp), 'YYYY-MM-DD') as period, \
|
||||
COUNT(*) as count \
|
||||
FROM \"{}\" WHERE {} \
|
||||
GROUP BY date_trunc('{}', (data->>'{}')::timestamp) \
|
||||
ORDER BY period",
|
||||
grain,
|
||||
clean_field,
|
||||
table_name,
|
||||
conditions.join(" AND "),
|
||||
grain,
|
||||
clean_field,
|
||||
);
|
||||
|
||||
Ok((sql, values))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1375,4 +1430,67 @@ mod tests {
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
// ===== build_timeseries_sql 测试 =====
|
||||
|
||||
#[test]
|
||||
fn test_build_timeseries_sql_day_grain() {
|
||||
let (sql, values) = DynamicTableManager::build_timeseries_sql(
|
||||
"plugin_test",
|
||||
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
|
||||
"occurred_at",
|
||||
"day",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(sql.contains("date_trunc('day'"), "应有 day 粒度");
|
||||
assert!(sql.contains("GROUP BY"), "应有 GROUP BY");
|
||||
assert!(sql.contains("ORDER BY period"), "应按 period 排序");
|
||||
assert_eq!(values.len(), 1, "仅 tenant_id");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_timeseries_sql_month_grain() {
|
||||
let (sql, _) = DynamicTableManager::build_timeseries_sql(
|
||||
"plugin_test",
|
||||
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
|
||||
"created_date",
|
||||
"month",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(sql.contains("date_trunc('month'"), "应有 month 粒度");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_timeseries_sql_with_date_range() {
|
||||
let (sql, values) = DynamicTableManager::build_timeseries_sql(
|
||||
"plugin_test",
|
||||
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
|
||||
"occurred_at",
|
||||
"week",
|
||||
Some("2026-01-01"),
|
||||
Some("2026-04-01"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(sql.contains("date_trunc('week'"), "应有 week 粒度");
|
||||
assert!(sql.contains(">="), "应有 start 条件");
|
||||
assert!(sql.contains("<"), "应有 end 条件");
|
||||
assert_eq!(values.len(), 3, "tenant_id + start + end");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_timeseries_sql_invalid_grain() {
|
||||
let result = DynamicTableManager::build_timeseries_sql(
|
||||
"plugin_test",
|
||||
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
|
||||
"occurred_at",
|
||||
"hour",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(result.is_err(), "不支持的 grain 应报错");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user