1#[derive(Debug, Clone, PartialEq)]
9pub enum ParsedStatement {
10 Select(SelectQuery),
12 Insert(InsertQuery),
14 Delete(DeleteQuery),
16 Unsupported(String),
18}
19
20#[derive(Debug, Clone, PartialEq)]
22pub struct SelectQuery {
23 pub agent_id: Option<String>,
25 pub query_text: Option<String>,
27 pub limit: usize,
29 pub offset: usize,
31 pub orientation_cache: bool,
36 pub reconstruct: bool,
43}
44
45#[derive(Debug, Clone, PartialEq)]
47pub struct InsertQuery {
48 pub content: String,
49 pub agent_id: Option<String>,
50 pub importance: Option<f32>,
51 pub memory_type: Option<String>,
52 pub tags: Vec<String>,
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub struct DeleteQuery {
58 pub memory_id: Option<String>,
60 pub agent_id: Option<String>,
62}
63
64pub fn parse_sql(sql: &str) -> ParsedStatement {
71 let trimmed = sql.trim().trim_end_matches(';');
72 let upper = trimmed.to_uppercase();
73
74 if upper.starts_with("SELECT") {
75 parse_select(trimmed)
76 } else if upper.starts_with("INSERT") {
77 parse_insert(trimmed)
78 } else if upper.starts_with("DELETE") {
79 parse_delete(trimmed)
80 } else {
81 ParsedStatement::Unsupported(trimmed.to_string())
82 }
83}
84
85fn parse_select(sql: &str) -> ParsedStatement {
86 let upper = sql.to_uppercase();
87 let mut query = SelectQuery {
88 agent_id: None,
89 query_text: None,
90 limit: 50,
91 offset: 0,
92 orientation_cache: upper.contains("/*+ ORIENTATION_CACHE")
93 || upper.contains("/*+ORIENTATION_CACHE"),
94 reconstruct: upper.contains("/*+ RECONSTRUCT") || upper.contains("/*+RECONSTRUCT"),
95 };
96
97 if let Some(pos) = upper.find("LIMIT") {
99 let after = &sql[pos + 5..].trim();
100 if let Some(num_str) = after.split_whitespace().next()
101 && let Ok(n) = num_str.parse::<usize>()
102 {
103 query.limit = n;
104 }
105 }
106
107 if let Some(pos) = upper.find("OFFSET") {
109 let after = &sql[pos + 6..].trim();
110 if let Some(num_str) = after.split_whitespace().next()
111 && let Ok(n) = num_str.parse::<usize>()
112 {
113 query.offset = n;
114 }
115 }
116
117 if let Some(agent_id) = extract_string_condition(&upper, sql, "AGENT_ID") {
119 query.agent_id = Some(agent_id);
120 }
121
122 if let Some(pos) = upper.find("CONTENT LIKE") {
124 let after = &sql[pos + 12..].trim();
125 if let Some(value) = extract_quoted_value(after) {
126 let clean = value.trim_matches('%').to_string();
128 if !clean.is_empty() {
129 query.query_text = Some(clean);
130 }
131 }
132 }
133
134 ParsedStatement::Select(query)
135}
136
137fn parse_insert(sql: &str) -> ParsedStatement {
138 let upper = sql.to_uppercase();
140
141 let cols_start = match upper.find('(') {
142 Some(p) => p,
143 None => return ParsedStatement::Unsupported(sql.to_string()),
144 };
145 let cols_end = match upper[cols_start..].find(')') {
146 Some(p) => cols_start + p,
147 None => return ParsedStatement::Unsupported(sql.to_string()),
148 };
149
150 let values_marker = match upper[cols_end..].find("VALUES") {
151 Some(p) => cols_end + p,
152 None => return ParsedStatement::Unsupported(sql.to_string()),
153 };
154
155 let vals_start = match upper[values_marker..].find('(') {
156 Some(p) => values_marker + p,
157 None => return ParsedStatement::Unsupported(sql.to_string()),
158 };
159 let vals_end = match sql[vals_start..].rfind(')') {
160 Some(p) => vals_start + p,
161 None => return ParsedStatement::Unsupported(sql.to_string()),
162 };
163
164 let columns: Vec<String> = sql[cols_start + 1..cols_end]
165 .split(',')
166 .map(|c| c.trim().to_uppercase())
167 .collect();
168
169 let values: Vec<String> = split_sql_values(&sql[vals_start + 1..vals_end]);
170
171 let mut insert = InsertQuery {
172 content: String::new(),
173 agent_id: None,
174 importance: None,
175 memory_type: None,
176 tags: vec![],
177 };
178
179 for (i, col) in columns.iter().enumerate() {
180 if i >= values.len() {
181 break;
182 }
183 let val = unquote(&values[i]);
184 match col.as_str() {
185 "CONTENT" => insert.content = val,
186 "AGENT_ID" => insert.agent_id = Some(val),
187 "IMPORTANCE" => insert.importance = val.parse().ok(),
188 "MEMORY_TYPE" => insert.memory_type = Some(val),
189 _ => {}
190 }
191 }
192
193 if insert.content.is_empty() {
194 return ParsedStatement::Unsupported(sql.to_string());
195 }
196
197 ParsedStatement::Insert(insert)
198}
199
200fn parse_delete(sql: &str) -> ParsedStatement {
201 let upper = sql.to_uppercase();
202 let mut delete = DeleteQuery {
203 memory_id: None,
204 agent_id: None,
205 };
206
207 if let Some(id) = extract_string_condition(&upper, sql, "ID") {
208 delete.memory_id = Some(id);
209 }
210 if let Some(agent_id) = extract_string_condition(&upper, sql, "AGENT_ID") {
211 delete.agent_id = Some(agent_id);
212 }
213
214 ParsedStatement::Delete(delete)
215}
216
217fn extract_string_condition(upper: &str, original: &str, column: &str) -> Option<String> {
219 let pattern = format!("{column} =");
220 if let Some(pos) = upper.find(&pattern) {
221 let after = &original[pos + pattern.len()..].trim_start();
222 return extract_quoted_value(after);
223 }
224 None
225}
226
227fn extract_quoted_value(s: &str) -> Option<String> {
229 let s = s.trim();
230 if let Some(stripped) = s.strip_prefix('\'')
231 && let Some(end) = stripped.find('\'')
232 {
233 return Some(stripped[..end].to_string());
234 }
235 None
236}
237
238fn split_sql_values(s: &str) -> Vec<String> {
240 let mut values = vec![];
241 let mut current = String::new();
242 let mut in_quote = false;
243
244 for ch in s.chars() {
245 match ch {
246 '\'' if !in_quote => {
247 in_quote = true;
248 current.push(ch);
249 }
250 '\'' if in_quote => {
251 in_quote = false;
252 current.push(ch);
253 }
254 ',' if !in_quote => {
255 values.push(current.trim().to_string());
256 current.clear();
257 }
258 _ => current.push(ch),
259 }
260 }
261
262 let trimmed = current.trim().to_string();
263 if !trimmed.is_empty() {
264 values.push(trimmed);
265 }
266 values
267}
268
269fn unquote(s: &str) -> String {
271 let trimmed = s.trim();
272 if (trimmed.starts_with('\'') && trimmed.ends_with('\''))
273 || (trimmed.starts_with('"') && trimmed.ends_with('"'))
274 {
275 trimmed[1..trimmed.len() - 1].to_string()
276 } else {
277 trimmed.to_string()
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_parse_select_basic() {
287 let stmt = parse_sql("SELECT * FROM memories LIMIT 10");
288 match stmt {
289 ParsedStatement::Select(q) => {
290 assert_eq!(q.limit, 10);
291 assert_eq!(q.offset, 0);
292 assert!(q.agent_id.is_none());
293 }
294 other => panic!("Expected Select, got {:?}", other),
295 }
296 }
297
298 #[test]
299 fn test_parse_select_with_where() {
300 let stmt = parse_sql("SELECT * FROM memories WHERE agent_id = 'bot-1' LIMIT 5");
301 match stmt {
302 ParsedStatement::Select(q) => {
303 assert_eq!(q.agent_id.as_deref(), Some("bot-1"));
304 assert_eq!(q.limit, 5);
305 }
306 other => panic!("Expected Select, got {:?}", other),
307 }
308 }
309
310 #[test]
311 fn test_parse_select_with_like() {
312 let stmt = parse_sql("SELECT * FROM memories WHERE content LIKE '%hello%' LIMIT 20");
313 match stmt {
314 ParsedStatement::Select(q) => {
315 assert_eq!(q.query_text.as_deref(), Some("hello"));
316 assert_eq!(q.limit, 20);
317 }
318 other => panic!("Expected Select, got {:?}", other),
319 }
320 }
321
322 #[test]
323 fn test_parse_insert() {
324 let stmt =
325 parse_sql("INSERT INTO memories (content, importance) VALUES ('test memory', 0.8)");
326 match stmt {
327 ParsedStatement::Insert(q) => {
328 assert_eq!(q.content, "test memory");
329 assert_eq!(q.importance, Some(0.8));
330 }
331 other => panic!("Expected Insert, got {:?}", other),
332 }
333 }
334
335 #[test]
336 fn test_parse_insert_with_agent() {
337 let stmt = parse_sql(
338 "INSERT INTO memories (content, agent_id, memory_type) VALUES ('data', 'agent-1', 'episodic')",
339 );
340 match stmt {
341 ParsedStatement::Insert(q) => {
342 assert_eq!(q.content, "data");
343 assert_eq!(q.agent_id.as_deref(), Some("agent-1"));
344 assert_eq!(q.memory_type.as_deref(), Some("episodic"));
345 }
346 other => panic!("Expected Insert, got {:?}", other),
347 }
348 }
349
350 #[test]
351 fn test_parse_delete() {
352 let stmt =
353 parse_sql("DELETE FROM memories WHERE id = '550e8400-e29b-41d4-a716-446655440000'");
354 match stmt {
355 ParsedStatement::Delete(q) => {
356 assert_eq!(
357 q.memory_id.as_deref(),
358 Some("550e8400-e29b-41d4-a716-446655440000")
359 );
360 }
361 other => panic!("Expected Delete, got {:?}", other),
362 }
363 }
364
365 #[test]
366 fn test_parse_unsupported() {
367 let stmt = parse_sql("DROP TABLE memories");
368 assert!(matches!(stmt, ParsedStatement::Unsupported(_)));
369 }
370
371 #[test]
372 fn test_parse_select_with_offset() {
373 let stmt = parse_sql("SELECT * FROM memories LIMIT 10 OFFSET 20");
374 match stmt {
375 ParsedStatement::Select(q) => {
376 assert_eq!(q.limit, 10);
377 assert_eq!(q.offset, 20);
378 }
379 other => panic!("Expected Select, got {:?}", other),
380 }
381 }
382}