use nodedb_sql::parser::preprocess::lex::find_ascii_case_insensitive;
use serde_json::{Map, Value as JsonValue};
use crate::control::server::response_shape::types::{DdlColType, ShapedRows};
use super::super::result::{DdlError, DdlResult};
pub fn execute_chunk_text(sql: &str) -> Result<Vec<DdlResult>, DdlError> {
let fn_pos = find_ascii_case_insensitive(sql, "NDB_CHUNK_TEXT(")
.ok_or_else(|| err("42601", "expected NDB_CHUNK_TEXT(...)"))?;
let paren_start = fn_pos
+ sql[fn_pos..]
.find('(')
.ok_or_else(|| err("42601", "expected NDB_CHUNK_TEXT(...)"))?;
let paren_end = sql
.rfind(')')
.ok_or_else(|| err("42601", "expected closing ) for NDB_CHUNK_TEXT"))?;
let inner = &sql[paren_start + 1..paren_end];
let mut text_val = String::new();
let mut chunk_size = 0usize;
let mut overlap = 0usize;
let mut strategy_str = String::from("character");
let args = split_args_respecting_quotes(inner);
if args.is_empty() {
return Err(err(
"42601",
"NDB_CHUNK_TEXT requires at least text and chunk_size arguments",
));
}
let is_named = args[0].contains("=>");
if is_named {
for arg in &args {
if let Some((key, val)) = arg.split_once("=>") {
let key = key.trim().to_lowercase();
let val = val.trim().trim_matches('\'').trim_matches('"');
match key.as_str() {
"text" => text_val = val.to_string(),
"chunk_size" => {
chunk_size = val
.parse()
.map_err(|_| err("22023", &format!("invalid chunk_size: {val}")))?;
}
"overlap" => {
overlap = val
.parse()
.map_err(|_| err("22023", &format!("invalid overlap: {val}")))?;
}
"strategy" => strategy_str = val.to_string(),
other => {
return Err(err(
"42601",
&format!("unknown NDB_CHUNK_TEXT parameter: {other}"),
));
}
}
}
}
} else {
if args.len() < 2 {
return Err(err(
"42601",
"NDB_CHUNK_TEXT requires at least (text, chunk_size)",
));
}
text_val = args[0]
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
chunk_size = args[1]
.trim()
.parse()
.map_err(|_| err("22023", "invalid chunk_size"))?;
if args.len() > 2 {
overlap = args[2]
.trim()
.parse()
.map_err(|_| err("22023", "invalid overlap"))?;
}
if args.len() > 3 {
strategy_str = args[3]
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
}
}
if text_val.is_empty() {
return Err(err("42601", "NDB_CHUNK_TEXT: text argument is required"));
}
if chunk_size == 0 {
return Err(err("42601", "NDB_CHUNK_TEXT: chunk_size must be > 0"));
}
let strategy = nodedb_query::ChunkStrategy::parse(&strategy_str).ok_or_else(|| {
err(
"22023",
&format!(
"unknown strategy '{strategy_str}'; supported: character, sentence, paragraph"
),
)
})?;
let chunks = nodedb_query::chunk_text(&text_val, chunk_size, overlap, strategy)
.map_err(|e| err("22023", &e.to_string()))?;
let columns = vec![
"index".to_string(),
"start".to_string(),
"end".to_string(),
"text".to_string(),
];
let column_types = vec![
DdlColType::Text,
DdlColType::Text,
DdlColType::Text,
DdlColType::Text,
];
let rows: Vec<Map<String, JsonValue>> = chunks
.iter()
.map(|c| {
let mut row = Map::new();
row.insert("index".to_string(), JsonValue::String(c.index.to_string()));
row.insert("start".to_string(), JsonValue::String(c.start.to_string()));
row.insert("end".to_string(), JsonValue::String(c.end.to_string()));
row.insert("text".to_string(), JsonValue::String(c.text.clone()));
row
})
.collect();
Ok(vec![DdlResult::Rows(ShapedRows {
columns,
column_types,
rows,
notice: None,
})])
}
fn err(sqlstate: &str, message: &str) -> DdlError {
DdlError {
sqlstate: sqlstate.to_string(),
message: message.to_string(),
}
}
fn split_args_respecting_quotes(s: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
let mut in_quote = false;
for ch in s.chars() {
match ch {
'\'' if !in_quote => {
in_quote = true;
current.push(ch);
}
'\'' if in_quote => {
in_quote = false;
current.push(ch);
}
',' if !in_quote => {
args.push(std::mem::take(&mut current));
}
_ => current.push(ch),
}
}
if !current.is_empty() {
args.push(current);
}
args
}