qualia_core_db/sparql_library/sparql_grammar/
update.rs1use std::collections::HashMap;
14
15use crate::sparql_ast::SparqlQueryContext;
16use crate::sparql_library::sparql_grammar::pattern::parse_where_group;
17use crate::sparql_library::sparql_grammar::tokenizer::{tokenize, Token};
18use crate::sparql_library::sparql_update::UpdateOperation;
19use crate::NQuin;
20
21pub fn is_update(query: &str) -> bool {
23 let up = query.trim_start().to_ascii_uppercase();
24 up.starts_with("INSERT")
25 || up.starts_with("DELETE")
26 || up.starts_with("CLEAR")
27 || up.starts_with("CREATE")
28 || up.starts_with("DROP")
29 || up.starts_with("LOAD")
30}
31
32pub fn parse_update(
35 input: &str,
36 ctx: &mut SparqlQueryContext,
37 prefixes: &HashMap<String, String>,
38) -> Result<UpdateOperation, String> {
39 let trimmed = input.trim();
40 let up = trimmed.to_ascii_uppercase();
41
42 if up.starts_with("INSERT DATA") {
43 let body = brace_body(trimmed, "INSERT DATA")?;
44 let (quins, quin_count) = parse_ground_triples(&body, prefixes)?;
45 Ok(UpdateOperation::InsertData { quins, quin_count })
46 } else if up.starts_with("DELETE DATA") {
47 let body = brace_body(trimmed, "DELETE DATA")?;
48 let (quins, quin_count) = parse_ground_triples(&body, prefixes)?;
49 Ok(UpdateOperation::DeleteData { quins, quin_count })
50 } else if up.starts_with("DELETE") && up.contains("WHERE") {
51 parse_delete_insert(trimmed, ctx, prefixes)
52 } else if up.starts_with("INSERT") && up.contains("WHERE") {
53 parse_delete_insert(trimmed, ctx, prefixes)
54 } else if up.starts_with("CLEAR") {
55 Ok(UpdateOperation::Clear {
56 graph: parse_graph_ref(trimmed, "CLEAR", prefixes),
57 })
58 } else if up.starts_with("CREATE") {
59 Ok(UpdateOperation::Create {
60 graph: parse_graph_ref(trimmed, "CREATE", prefixes),
61 })
62 } else if up.starts_with("DROP") {
63 Ok(UpdateOperation::Drop {
64 graph: parse_graph_ref(trimmed, "DROP", prefixes),
65 })
66 } else if up.starts_with("LOAD") {
67 parse_load(trimmed, prefixes)
68 } else {
69 Err("unrecognised SPARQL Update operation".to_string())
70 }
71}
72
73fn brace_body(input: &str, keyword: &str) -> Result<String, String> {
75 let after = input[keyword.len()..].trim_start();
76 let open = after
77 .find('{')
78 .ok_or_else(|| format!("expected '{{' after {keyword}"))?;
79 let bytes = after.as_bytes();
80 let mut depth = 0i32;
81 let mut i = open;
82 let mut end = None;
83 while i < bytes.len() {
84 match bytes[i] {
85 b'{' => depth += 1,
86 b'}' => {
87 depth -= 1;
88 if depth == 0 {
89 end = Some(i);
90 break;
91 }
92 }
93 _ => {}
94 }
95 i += 1;
96 }
97 let end = end.ok_or_else(|| format!("unbalanced braces after {keyword}"))?;
98 Ok(after[open + 1..end].to_string())
99}
100
101fn parse_ground_triples(
104 body: &str,
105 prefixes: &HashMap<String, String>,
106) -> Result<([NQuin; 64], u8), String> {
107 let tokens = tokenize(body)?;
108 let mut quins = [NQuin::default(); 64];
109 let mut count = 0usize;
110 let mut i = 0usize;
111
112 let mut terms: Vec<u64> = Vec::new();
114 while i < tokens.len() {
115 match &tokens[i] {
116 Token::Punct('.') => {
117 i += 1;
118 }
119 Token::Var(_) => {
120 return Err("INSERT/DELETE DATA must be ground (no variables)".to_string());
121 }
122 tok => {
123 terms.push(term_hash(tok, prefixes)?);
124 i += 1;
125 if terms.len() == 3 {
126 if count >= 64 {
127 return Err("too many triples in a DATA block (max 64)".to_string());
128 }
129 quins[count] = make_quin(terms[0], terms[1], terms[2]);
130 count += 1;
131 terms.clear();
132 }
133 }
134 }
135 }
136 if !terms.is_empty() {
137 return Err("trailing incomplete triple in DATA block".to_string());
138 }
139 if count == 0 {
140 return Err("DATA block contains no triples".to_string());
141 }
142 Ok((quins, count as u8))
143}
144
145fn parse_delete_insert(
146 input: &str,
147 ctx: &mut SparqlQueryContext,
148 prefixes: &HashMap<String, String>,
149) -> Result<UpdateOperation, String> {
150 let delete_pattern = if let Some(pos) = ci_find(input, "DELETE") {
152 let body = brace_body(&input[pos..], "DELETE")?;
154 Some(parse_group_text(&body, ctx, prefixes)?)
155 } else {
156 None
157 };
158 let insert_pattern = if let Some(pos) = ci_find(input, "INSERT") {
159 let body = brace_body(&input[pos..], "INSERT")?;
160 Some(parse_group_text(&body, ctx, prefixes)?)
161 } else {
162 None
163 };
164 let where_pos = ci_find(input, "WHERE").ok_or("DELETE/INSERT requires a WHERE clause")?;
165 let where_body = brace_body(&input[where_pos..], "WHERE")?;
166 let where_pattern = parse_group_text(&where_body, ctx, prefixes)?;
167
168 Ok(UpdateOperation::DeleteInsert {
169 delete_pattern: delete_pattern.unwrap_or(where_pattern),
170 insert_pattern: insert_pattern.unwrap_or(where_pattern),
171 where_pattern,
172 })
173}
174
175fn parse_group_text(
178 body: &str,
179 ctx: &mut SparqlQueryContext,
180 prefixes: &HashMap<String, String>,
181) -> Result<crate::sparql_ast::PatternId, String> {
182 let wrapped = format!("{{ {body} }}");
183 parse_where_group(&wrapped, ctx, prefixes)
184}
185
186fn parse_graph_ref(input: &str, keyword: &str, prefixes: &HashMap<String, String>) -> u64 {
187 let rest = input[keyword.len()..].trim();
189 let rest_up = rest.to_ascii_uppercase();
190 if rest_up.starts_with("DEFAULT") || rest_up.starts_with("ALL") || rest.is_empty() {
191 return 0;
192 }
193 if let Ok(tokens) = tokenize(rest) {
194 for t in &tokens {
195 if let Token::Iri(_) | Token::Prefixed(_, _) = t {
196 if let Ok(h) = term_hash(t, prefixes) {
197 return h;
198 }
199 }
200 }
201 }
202 0
203}
204
205fn parse_load(input: &str, prefixes: &HashMap<String, String>) -> Result<UpdateOperation, String> {
206 let tokens = tokenize(&input[4..])?;
207 let mut uri = 0u64;
208 let mut graph = 0u64;
209 let mut seen_into = false;
210 for t in &tokens {
211 match t {
212 Token::Word(w) if w.eq_ignore_ascii_case("INTO") => seen_into = true,
213 Token::Iri(_) | Token::Prefixed(_, _) => {
214 let h = term_hash(t, prefixes)?;
215 if seen_into {
216 graph = h;
217 } else {
218 uri = h;
219 }
220 }
221 _ => {}
222 }
223 }
224 Ok(UpdateOperation::Load { uri, graph })
225}
226
227fn ci_find(haystack: &str, needle: &str) -> Option<usize> {
229 let h = haystack.to_ascii_uppercase();
230 h.find(&needle.to_ascii_uppercase())
231}
232
233fn term_hash(tok: &Token, prefixes: &HashMap<String, String>) -> Result<u64, String> {
234 match tok {
235 Token::Iri(iri) => Ok(crate::lexicon::generate_60bit_token(iri.as_bytes())),
236 Token::Prefixed(prefix, local) => {
237 let expanded = match prefixes.get(prefix) {
238 Some(base) => format!("{base}{local}"),
239 None => format!("{prefix}:{local}"),
240 };
241 Ok(crate::lexicon::generate_60bit_token(expanded.as_bytes()))
242 }
243 Token::Str { value, .. } => Ok(crate::lexicon::generate_60bit_token(value.as_bytes())),
244 Token::Num(text) => Ok(text
245 .parse::<u64>()
246 .unwrap_or_else(|_| crate::lexicon::generate_60bit_token(text.as_bytes()))),
247 Token::Bool(b) => Ok(crate::lexicon::generate_60bit_token(if *b {
248 b"true"
249 } else {
250 b"false"
251 })),
252 Token::Word(w) if w == "a" => Ok(crate::lexicon::generate_60bit_token(
253 b"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
254 )),
255 Token::Word(w) => Ok(crate::lexicon::generate_60bit_token(w.as_bytes())),
256 other => Err(format!("invalid term in update: {other:?}")),
257 }
258}
259
260fn make_quin(subject: u64, predicate: u64, object: u64) -> NQuin {
261 let mut q = NQuin {
262 subject,
263 predicate,
264 object,
265 context: 0,
266 metadata: 0,
267 parity: 0,
268 };
269 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
270 q
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 fn ctx() -> SparqlQueryContext {
278 SparqlQueryContext::new()
279 }
280
281 #[test]
282 fn detects_update_vs_query() {
283 assert!(is_update("INSERT DATA { <a> <b> <c> }"));
284 assert!(is_update("DELETE WHERE { ?s ?p ?o }"));
285 assert!(!is_update("SELECT ?s WHERE { ?s ?p ?o }"));
286 }
287
288 #[test]
289 fn parses_insert_data() {
290 let mut c = ctx();
291 let op = parse_update(
292 "INSERT DATA { <http://a> <http://b> <http://c> . <http://d> <http://e> <http://f> }",
293 &mut c,
294 &HashMap::new(),
295 )
296 .unwrap();
297 match op {
298 UpdateOperation::InsertData { quin_count, quins } => {
299 assert_eq!(quin_count, 2);
300 assert_ne!(quins[0].subject, 0);
301 assert_eq!(
302 quins[0].parity,
303 quins[0].subject ^ quins[0].predicate ^ quins[0].object
304 );
305 }
306 other => panic!("expected InsertData, got {other:?}"),
307 }
308 }
309
310 #[test]
311 fn parses_delete_data() {
312 let mut c = ctx();
313 let op = parse_update(
314 "DELETE DATA { <http://a> <http://b> <http://c> }",
315 &mut c,
316 &HashMap::new(),
317 )
318 .unwrap();
319 assert!(matches!(
320 op,
321 UpdateOperation::DeleteData { quin_count: 1, .. }
322 ));
323 }
324
325 #[test]
326 fn insert_data_rejects_variables() {
327 let mut c = ctx();
328 let err = parse_update("INSERT DATA { ?s <b> <c> }", &mut c, &HashMap::new()).unwrap_err();
329 assert!(err.contains("ground"), "got {err}");
330 }
331
332 #[test]
333 fn parses_delete_insert_where() {
334 let mut c = ctx();
335 let op = parse_update(
336 "DELETE { ?s <http://old> ?o } INSERT { ?s <http://new> ?o } WHERE { ?s <http://old> ?o }",
337 &mut c,
338 &HashMap::new(),
339 )
340 .unwrap();
341 assert!(matches!(op, UpdateOperation::DeleteInsert { .. }));
342 }
343
344 #[test]
345 fn parses_clear_and_drop() {
346 let mut c = ctx();
347 assert!(matches!(
348 parse_update("CLEAR GRAPH <http://g>", &mut c, &HashMap::new()).unwrap(),
349 UpdateOperation::Clear { .. }
350 ));
351 assert!(matches!(
352 parse_update("DROP GRAPH <http://g>", &mut c, &HashMap::new()).unwrap(),
353 UpdateOperation::Drop { .. }
354 ));
355 assert!(matches!(
356 parse_update("CLEAR DEFAULT", &mut c, &HashMap::new()).unwrap(),
357 UpdateOperation::Clear { graph: 0 }
358 ));
359 }
360}