use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::state::SharedState;
use crate::event::cdc::stream_def::RetentionConfig;
use crate::event::topic::TopicDef;
use nodedb_sql::parser::preprocess::lex::{find_ascii_case_insensitive, find_ascii_keyword};
use super::super::super::result::{DdlError, DdlResult};
use super::super::auth_support::{require_tenant_admin, status};
pub fn create_topic(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
sql: &str,
) -> Result<Vec<DdlResult>, DdlError> {
require_tenant_admin(identity, "create topics")?;
if parts.len() < 3 {
return Err(DdlError {
sqlstate: "42601".to_string(),
message: "expected CREATE TOPIC <name>".to_string(),
});
}
let name = parts[2].to_lowercase();
let tenant_id = identity.tenant_id.as_u64();
if state.ep_topic_registry.get(tenant_id, &name).is_some() {
return Err(DdlError {
sqlstate: "42710".to_string(),
message: format!("topic '{name}' already exists"),
});
}
let mut retention = RetentionConfig {
max_events: 10_000,
max_age_secs: 3_600, };
if let Some(with_pos) = find_ascii_keyword(sql, "WITH") {
let with_section = sql[with_pos + 4..].trim();
if let Some(inner) = with_section
.strip_prefix('(')
.and_then(|s| s.split_once(')'))
.map(|(inner, _)| inner)
&& let Some(ret_pos) = find_ascii_case_insensitive(inner, "RETENTION")
{
let after = inner[ret_pos + 9..].trim().trim_start_matches('=').trim();
let val = after
.trim_start_matches('\'')
.split('\'')
.next()
.unwrap_or("");
retention.max_age_secs = parse_duration_secs(val);
}
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| DdlError {
sqlstate: "XX000".to_string(),
message: "system clock error".to_string(),
})?
.as_secs();
let def = TopicDef {
tenant_id,
name: name.clone(),
retention,
owner: identity.username.clone(),
created_at: now,
};
let catalog = state.credentials.catalog();
catalog.put_ep_topic(&def).map_err(|e| DdlError {
sqlstate: "XX000".to_string(),
message: format!("catalog write: {e}"),
})?;
state.ep_topic_registry.register(def);
state.audit_record(
crate::control::security::audit::AuditEvent::AdminAction,
Some(identity.tenant_id),
&identity.username,
&format!("CREATE TOPIC {name}"),
);
Ok(status("CREATE TOPIC"))
}
fn parse_duration_secs(s: &str) -> u64 {
let s = s.trim().to_lowercase();
if let Some(h) = s.strip_suffix("hour").or_else(|| s.strip_suffix("hours")) {
return h.trim().parse::<u64>().unwrap_or(1) * 3600;
}
if let Some(m) = s
.strip_suffix("minute")
.or_else(|| s.strip_suffix("minutes"))
{
return m.trim().parse::<u64>().unwrap_or(1) * 60;
}
if let Some(d) = s.strip_suffix("day").or_else(|| s.strip_suffix("days")) {
return d.trim().parse::<u64>().unwrap_or(1) * 86_400;
}
s.parse::<u64>().unwrap_or(3_600)
}