Skip to main content

oxirs_core/sparql/
update_parser.rs

1//! SPARQL UPDATE statement parser.
2//!
3//! Parses SPARQL 1.1 Update operations including INSERT DATA, DELETE DATA,
4//! DELETE/INSERT WHERE, LOAD, CLEAR, DROP, CREATE, COPY, MOVE, and ADD.
5//! Reports parse errors with position information.
6
7use std::collections::HashMap;
8use std::fmt;
9
10// ────────────────────────────────────────────────────────────────────────────
11// Error types
12// ────────────────────────────────────────────────────────────────────────────
13
14/// Error produced when parsing a SPARQL Update request fails.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct UpdateParseError {
17    /// Human-readable description of the error.
18    pub message: String,
19    /// Byte offset in the input where the error was detected.
20    pub position: usize,
21    /// Optional line number (1-based) for display purposes.
22    pub line: Option<usize>,
23    /// Optional column number (1-based) for display purposes.
24    pub column: Option<usize>,
25}
26
27impl UpdateParseError {
28    /// Create a new parse error at the given byte offset.
29    pub fn new(message: impl Into<String>, position: usize) -> Self {
30        Self {
31            message: message.into(),
32            position,
33            line: None,
34            column: None,
35        }
36    }
37
38    /// Attach line/column information derived from the original source.
39    pub fn with_location(mut self, line: usize, column: usize) -> Self {
40        self.line = Some(line);
41        self.column = Some(column);
42        self
43    }
44}
45
46impl fmt::Display for UpdateParseError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match (self.line, self.column) {
49            (Some(ln), Some(col)) => {
50                write!(f, "update parse error at {}:{}: {}", ln, col, self.message)
51            }
52            _ => write!(
53                f,
54                "update parse error at byte {}: {}",
55                self.position, self.message
56            ),
57        }
58    }
59}
60
61impl std::error::Error for UpdateParseError {}
62
63/// Compute (line, column) from a byte offset and the original source text.
64fn line_col(source: &str, byte_offset: usize) -> (usize, usize) {
65    let prefix = &source[..byte_offset.min(source.len())];
66    let line = prefix.chars().filter(|&c| c == '\n').count() + 1;
67    let last_newline = prefix.rfind('\n').map(|p| p + 1).unwrap_or(0);
68    let col = byte_offset.saturating_sub(last_newline) + 1;
69    (line, col)
70}
71
72// ────────────────────────────────────────────────────────────────────────────
73// AST types
74// ────────────────────────────────────────────────────────────────────────────
75
76/// A single triple pattern inside a SPARQL Update template or WHERE clause.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct TriplePattern {
79    pub subject: String,
80    pub predicate: String,
81    pub object: String,
82}
83
84impl TriplePattern {
85    pub fn new(
86        subject: impl Into<String>,
87        predicate: impl Into<String>,
88        object: impl Into<String>,
89    ) -> Self {
90        Self {
91            subject: subject.into(),
92            predicate: predicate.into(),
93            object: object.into(),
94        }
95    }
96}
97
98/// Target graph specification used in CLEAR / DROP / COPY / MOVE / ADD.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum GraphTarget {
101    /// A specific named graph identified by IRI.
102    Graph(String),
103    /// The default graph.
104    Default,
105    /// All named graphs.
106    Named,
107    /// Default graph + all named graphs.
108    All,
109}
110
111impl fmt::Display for GraphTarget {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            GraphTarget::Graph(iri) => write!(f, "GRAPH <{}>", iri),
115            GraphTarget::Default => write!(f, "DEFAULT"),
116            GraphTarget::Named => write!(f, "NAMED"),
117            GraphTarget::All => write!(f, "ALL"),
118        }
119    }
120}
121
122/// A parsed SPARQL Update operation.
123#[derive(Debug, Clone, PartialEq)]
124pub enum UpdateOperation {
125    /// `INSERT DATA { triples }`
126    InsertData {
127        /// Triples to insert.
128        triples: Vec<TriplePattern>,
129        /// Optional target graph IRI.
130        graph: Option<String>,
131    },
132    /// `DELETE DATA { triples }`
133    DeleteData {
134        /// Triples to delete.
135        triples: Vec<TriplePattern>,
136        /// Optional target graph IRI.
137        graph: Option<String>,
138    },
139    /// `DELETE { del } INSERT { ins } WHERE { pattern }`
140    DeleteInsertWhere {
141        delete_triples: Vec<TriplePattern>,
142        insert_triples: Vec<TriplePattern>,
143        where_triples: Vec<TriplePattern>,
144        graph: Option<String>,
145    },
146    /// `LOAD <iri> [INTO GRAPH <target>]`
147    Load {
148        source_uri: String,
149        target_graph: Option<String>,
150        silent: bool,
151    },
152    /// `CLEAR target`
153    Clear { target: GraphTarget, silent: bool },
154    /// `DROP target`
155    Drop { target: GraphTarget, silent: bool },
156    /// `CREATE GRAPH <iri>`
157    CreateGraph { graph_iri: String, silent: bool },
158    /// `COPY source TO target`
159    Copy {
160        source: GraphTarget,
161        destination: GraphTarget,
162        silent: bool,
163    },
164    /// `MOVE source TO target`
165    Move {
166        source: GraphTarget,
167        destination: GraphTarget,
168        silent: bool,
169    },
170    /// `ADD source TO target`
171    Add {
172        source: GraphTarget,
173        destination: GraphTarget,
174        silent: bool,
175    },
176}
177
178impl UpdateOperation {
179    /// A short human-readable label for the operation kind.
180    pub fn kind_label(&self) -> &'static str {
181        match self {
182            UpdateOperation::InsertData { .. } => "INSERT DATA",
183            UpdateOperation::DeleteData { .. } => "DELETE DATA",
184            UpdateOperation::DeleteInsertWhere { .. } => "DELETE/INSERT WHERE",
185            UpdateOperation::Load { .. } => "LOAD",
186            UpdateOperation::Clear { .. } => "CLEAR",
187            UpdateOperation::Drop { .. } => "DROP",
188            UpdateOperation::CreateGraph { .. } => "CREATE GRAPH",
189            UpdateOperation::Copy { .. } => "COPY",
190            UpdateOperation::Move { .. } => "MOVE",
191            UpdateOperation::Add { .. } => "ADD",
192        }
193    }
194}
195
196/// A fully parsed SPARQL Update request (one or more operations separated by `;`).
197#[derive(Debug, Clone, PartialEq)]
198pub struct UpdateRequest {
199    /// PREFIX declarations.
200    pub prefixes: HashMap<String, String>,
201    /// Ordered list of update operations.
202    pub operations: Vec<UpdateOperation>,
203}
204
205// ────────────────────────────────────────────────────────────────────────────
206// Parser
207// ────────────────────────────────────────────────────────────────────────────
208
209/// SPARQL UPDATE parser.
210pub struct UpdateParser {
211    /// Namespace prefix map built from PREFIX declarations.
212    prefixes: HashMap<String, String>,
213}
214
215impl Default for UpdateParser {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221impl UpdateParser {
222    /// Create a new parser with an empty prefix map.
223    pub fn new() -> Self {
224        Self {
225            prefixes: HashMap::new(),
226        }
227    }
228
229    /// Create a parser pre-loaded with prefix definitions.
230    pub fn with_prefixes(prefixes: HashMap<String, String>) -> Self {
231        Self { prefixes }
232    }
233
234    /// Parse a full SPARQL Update request string.
235    pub fn parse(&mut self, input: &str) -> Result<UpdateRequest, UpdateParseError> {
236        let mut pos = 0usize;
237        let mut operations = Vec::new();
238
239        // Parse prologue (BASE / PREFIX declarations).
240        pos = self.skip_ws(input, pos);
241        pos = self.parse_prologue(input, pos)?;
242
243        // Parse one or more update operations separated by `;`.
244        loop {
245            pos = self.skip_ws(input, pos);
246            if pos >= input.len() {
247                break;
248            }
249
250            let op = self.parse_operation(input, &mut pos)?;
251            operations.push(op);
252
253            pos = self.skip_ws(input, pos);
254            if pos < input.len() && input.as_bytes().get(pos) == Some(&b';') {
255                pos += 1; // consume separator
256            }
257        }
258
259        if operations.is_empty() {
260            let (ln, col) = line_col(input, pos);
261            return Err(UpdateParseError::new("empty update request", pos).with_location(ln, col));
262        }
263
264        Ok(UpdateRequest {
265            prefixes: self.prefixes.clone(),
266            operations,
267        })
268    }
269
270    // ── prologue ────────────────────────────────────────────────────────────
271
272    fn parse_prologue(&mut self, input: &str, mut pos: usize) -> Result<usize, UpdateParseError> {
273        loop {
274            pos = self.skip_ws(input, pos);
275            if self.match_keyword(input, pos, "PREFIX") {
276                pos = self.consume_keyword(input, pos, "PREFIX")?;
277                pos = self.skip_ws(input, pos);
278
279                let (prefix, new_pos) = self.read_prefix_label(input, pos)?;
280                pos = self.skip_ws(input, new_pos);
281
282                let (iri, new_pos) = self.read_iri_ref(input, pos)?;
283                pos = new_pos;
284
285                self.prefixes.insert(prefix, iri);
286            } else if self.match_keyword(input, pos, "BASE") {
287                pos = self.consume_keyword(input, pos, "BASE")?;
288                pos = self.skip_ws(input, pos);
289                // Read the IRI but we don't use BASE resolution in this simple parser.
290                let (_, new_pos) = self.read_iri_ref(input, pos)?;
291                pos = new_pos;
292            } else {
293                break;
294            }
295        }
296        Ok(pos)
297    }
298
299    // ── operation dispatch ──────────────────────────────────────────────────
300
301    fn parse_operation(
302        &self,
303        input: &str,
304        pos: &mut usize,
305    ) -> Result<UpdateOperation, UpdateParseError> {
306        *pos = self.skip_ws(input, *pos);
307        if *pos >= input.len() {
308            let (ln, col) = line_col(input, *pos);
309            return Err(
310                UpdateParseError::new("unexpected end of input", *pos).with_location(ln, col)
311            );
312        }
313
314        // Determine which operation keyword is at the current position.
315        if self.match_keyword(input, *pos, "INSERT") {
316            self.parse_insert(input, pos)
317        } else if self.match_keyword(input, *pos, "DELETE") {
318            self.parse_delete(input, pos)
319        } else if self.match_keyword(input, *pos, "LOAD") {
320            self.parse_load(input, pos)
321        } else if self.match_keyword(input, *pos, "CLEAR") {
322            self.parse_clear(input, pos)
323        } else if self.match_keyword(input, *pos, "DROP") {
324            self.parse_drop(input, pos)
325        } else if self.match_keyword(input, *pos, "CREATE") {
326            self.parse_create(input, pos)
327        } else if self.match_keyword(input, *pos, "COPY") {
328            self.parse_copy(input, pos)
329        } else if self.match_keyword(input, *pos, "MOVE") {
330            self.parse_move(input, pos)
331        } else if self.match_keyword(input, *pos, "ADD") {
332            self.parse_add(input, pos)
333        } else {
334            let (ln, col) = line_col(input, *pos);
335            let snippet: String = input[*pos..].chars().take(20).collect();
336            Err(UpdateParseError::new(
337                format!("expected update keyword, found: '{}'", snippet),
338                *pos,
339            )
340            .with_location(ln, col))
341        }
342    }
343
344    // ── INSERT ──────────────────────────────────────────────────────────────
345
346    fn parse_insert(
347        &self,
348        input: &str,
349        pos: &mut usize,
350    ) -> Result<UpdateOperation, UpdateParseError> {
351        *pos = self.consume_keyword(input, *pos, "INSERT")?;
352        *pos = self.skip_ws(input, *pos);
353
354        if self.match_keyword(input, *pos, "DATA") {
355            *pos = self.consume_keyword(input, *pos, "DATA")?;
356            *pos = self.skip_ws(input, *pos);
357
358            let (graph, triples) = self.parse_quad_data(input, pos)?;
359            Ok(UpdateOperation::InsertData { triples, graph })
360        } else {
361            // INSERT { ... } WHERE { ... }   (only insert part of delete/insert)
362            let (ln, col) = line_col(input, *pos);
363            Err(UpdateParseError::new(
364                "expected DATA after INSERT (standalone INSERT without DELETE is not supported here; use DELETE/INSERT WHERE)",
365                *pos,
366            )
367            .with_location(ln, col))
368        }
369    }
370
371    // ── DELETE ──────────────────────────────────────────────────────────────
372
373    fn parse_delete(
374        &self,
375        input: &str,
376        pos: &mut usize,
377    ) -> Result<UpdateOperation, UpdateParseError> {
378        *pos = self.consume_keyword(input, *pos, "DELETE")?;
379        *pos = self.skip_ws(input, *pos);
380
381        if self.match_keyword(input, *pos, "DATA") {
382            *pos = self.consume_keyword(input, *pos, "DATA")?;
383            *pos = self.skip_ws(input, *pos);
384
385            let (graph, triples) = self.parse_quad_data(input, pos)?;
386            Ok(UpdateOperation::DeleteData { triples, graph })
387        } else if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'{') {
388            // DELETE { ... } INSERT { ... } WHERE { ... }
389            let delete_triples = self.parse_brace_block(input, pos)?;
390            *pos = self.skip_ws(input, *pos);
391
392            let mut insert_triples = Vec::new();
393            if self.match_keyword(input, *pos, "INSERT") {
394                *pos = self.consume_keyword(input, *pos, "INSERT")?;
395                *pos = self.skip_ws(input, *pos);
396                insert_triples = self.parse_brace_block(input, pos)?;
397                *pos = self.skip_ws(input, *pos);
398            }
399
400            *pos = self.consume_keyword(input, *pos, "WHERE")?;
401            *pos = self.skip_ws(input, *pos);
402            let where_triples = self.parse_brace_block(input, pos)?;
403
404            Ok(UpdateOperation::DeleteInsertWhere {
405                delete_triples,
406                insert_triples,
407                where_triples,
408                graph: None,
409            })
410        } else {
411            let (ln, col) = line_col(input, *pos);
412            Err(
413                UpdateParseError::new("expected DATA or '{' after DELETE", *pos)
414                    .with_location(ln, col),
415            )
416        }
417    }
418
419    // ── LOAD ────────────────────────────────────────────────────────────────
420
421    fn parse_load(
422        &self,
423        input: &str,
424        pos: &mut usize,
425    ) -> Result<UpdateOperation, UpdateParseError> {
426        *pos = self.consume_keyword(input, *pos, "LOAD")?;
427        *pos = self.skip_ws(input, *pos);
428
429        let silent = self.try_consume_keyword(input, pos, "SILENT");
430        *pos = self.skip_ws(input, *pos);
431
432        let (source_uri, new_pos) = self.read_iri_ref(input, *pos)?;
433        *pos = new_pos;
434        *pos = self.skip_ws(input, *pos);
435
436        let target_graph = if self.match_keyword(input, *pos, "INTO") {
437            *pos = self.consume_keyword(input, *pos, "INTO")?;
438            *pos = self.skip_ws(input, *pos);
439            *pos = self.consume_keyword(input, *pos, "GRAPH")?;
440            *pos = self.skip_ws(input, *pos);
441            let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
442            *pos = new_pos;
443            Some(iri)
444        } else {
445            None
446        };
447
448        Ok(UpdateOperation::Load {
449            source_uri,
450            target_graph,
451            silent,
452        })
453    }
454
455    // ── CLEAR / DROP ────────────────────────────────────────────────────────
456
457    fn parse_clear(
458        &self,
459        input: &str,
460        pos: &mut usize,
461    ) -> Result<UpdateOperation, UpdateParseError> {
462        *pos = self.consume_keyword(input, *pos, "CLEAR")?;
463        *pos = self.skip_ws(input, *pos);
464        let silent = self.try_consume_keyword(input, pos, "SILENT");
465        *pos = self.skip_ws(input, *pos);
466        let target = self.parse_graph_ref_all(input, pos)?;
467        Ok(UpdateOperation::Clear { target, silent })
468    }
469
470    fn parse_drop(
471        &self,
472        input: &str,
473        pos: &mut usize,
474    ) -> Result<UpdateOperation, UpdateParseError> {
475        *pos = self.consume_keyword(input, *pos, "DROP")?;
476        *pos = self.skip_ws(input, *pos);
477        let silent = self.try_consume_keyword(input, pos, "SILENT");
478        *pos = self.skip_ws(input, *pos);
479        let target = self.parse_graph_ref_all(input, pos)?;
480        Ok(UpdateOperation::Drop { target, silent })
481    }
482
483    // ── CREATE ──────────────────────────────────────────────────────────────
484
485    fn parse_create(
486        &self,
487        input: &str,
488        pos: &mut usize,
489    ) -> Result<UpdateOperation, UpdateParseError> {
490        *pos = self.consume_keyword(input, *pos, "CREATE")?;
491        *pos = self.skip_ws(input, *pos);
492        let silent = self.try_consume_keyword(input, pos, "SILENT");
493        *pos = self.skip_ws(input, *pos);
494        *pos = self.consume_keyword(input, *pos, "GRAPH")?;
495        *pos = self.skip_ws(input, *pos);
496        let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
497        *pos = new_pos;
498        Ok(UpdateOperation::CreateGraph {
499            graph_iri: iri,
500            silent,
501        })
502    }
503
504    // ── COPY / MOVE / ADD ───────────────────────────────────────────────────
505
506    fn parse_copy(
507        &self,
508        input: &str,
509        pos: &mut usize,
510    ) -> Result<UpdateOperation, UpdateParseError> {
511        *pos = self.consume_keyword(input, *pos, "COPY")?;
512        *pos = self.skip_ws(input, *pos);
513        let silent = self.try_consume_keyword(input, pos, "SILENT");
514        *pos = self.skip_ws(input, *pos);
515        let source = self.parse_graph_or_default(input, pos)?;
516        *pos = self.skip_ws(input, *pos);
517        *pos = self.consume_keyword(input, *pos, "TO")?;
518        *pos = self.skip_ws(input, *pos);
519        let destination = self.parse_graph_or_default(input, pos)?;
520        Ok(UpdateOperation::Copy {
521            source,
522            destination,
523            silent,
524        })
525    }
526
527    fn parse_move(
528        &self,
529        input: &str,
530        pos: &mut usize,
531    ) -> Result<UpdateOperation, UpdateParseError> {
532        *pos = self.consume_keyword(input, *pos, "MOVE")?;
533        *pos = self.skip_ws(input, *pos);
534        let silent = self.try_consume_keyword(input, pos, "SILENT");
535        *pos = self.skip_ws(input, *pos);
536        let source = self.parse_graph_or_default(input, pos)?;
537        *pos = self.skip_ws(input, *pos);
538        *pos = self.consume_keyword(input, *pos, "TO")?;
539        *pos = self.skip_ws(input, *pos);
540        let destination = self.parse_graph_or_default(input, pos)?;
541        Ok(UpdateOperation::Move {
542            source,
543            destination,
544            silent,
545        })
546    }
547
548    fn parse_add(&self, input: &str, pos: &mut usize) -> Result<UpdateOperation, UpdateParseError> {
549        *pos = self.consume_keyword(input, *pos, "ADD")?;
550        *pos = self.skip_ws(input, *pos);
551        let silent = self.try_consume_keyword(input, pos, "SILENT");
552        *pos = self.skip_ws(input, *pos);
553        let source = self.parse_graph_or_default(input, pos)?;
554        *pos = self.skip_ws(input, *pos);
555        *pos = self.consume_keyword(input, *pos, "TO")?;
556        *pos = self.skip_ws(input, *pos);
557        let destination = self.parse_graph_or_default(input, pos)?;
558        Ok(UpdateOperation::Add {
559            source,
560            destination,
561            silent,
562        })
563    }
564
565    // ── graph references ────────────────────────────────────────────────────
566
567    fn parse_graph_ref_all(
568        &self,
569        input: &str,
570        pos: &mut usize,
571    ) -> Result<GraphTarget, UpdateParseError> {
572        *pos = self.skip_ws(input, *pos);
573        if self.match_keyword(input, *pos, "ALL") {
574            *pos = self.consume_keyword(input, *pos, "ALL")?;
575            Ok(GraphTarget::All)
576        } else if self.match_keyword(input, *pos, "DEFAULT") {
577            *pos = self.consume_keyword(input, *pos, "DEFAULT")?;
578            Ok(GraphTarget::Default)
579        } else if self.match_keyword(input, *pos, "NAMED") {
580            *pos = self.consume_keyword(input, *pos, "NAMED")?;
581            Ok(GraphTarget::Named)
582        } else if self.match_keyword(input, *pos, "GRAPH") {
583            *pos = self.consume_keyword(input, *pos, "GRAPH")?;
584            *pos = self.skip_ws(input, *pos);
585            let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
586            *pos = new_pos;
587            Ok(GraphTarget::Graph(iri))
588        } else {
589            let (ln, col) = line_col(input, *pos);
590            Err(
591                UpdateParseError::new("expected GRAPH, DEFAULT, NAMED, or ALL", *pos)
592                    .with_location(ln, col),
593            )
594        }
595    }
596
597    fn parse_graph_or_default(
598        &self,
599        input: &str,
600        pos: &mut usize,
601    ) -> Result<GraphTarget, UpdateParseError> {
602        *pos = self.skip_ws(input, *pos);
603        if self.match_keyword(input, *pos, "DEFAULT") {
604            *pos = self.consume_keyword(input, *pos, "DEFAULT")?;
605            Ok(GraphTarget::Default)
606        } else if self.match_keyword(input, *pos, "GRAPH") {
607            *pos = self.consume_keyword(input, *pos, "GRAPH")?;
608            *pos = self.skip_ws(input, *pos);
609            let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
610            *pos = new_pos;
611            Ok(GraphTarget::Graph(iri))
612        } else {
613            // Try to read a bare IRI as GRAPH <iri>
614            if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'<') {
615                let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
616                *pos = new_pos;
617                Ok(GraphTarget::Graph(iri))
618            } else {
619                let (ln, col) = line_col(input, *pos);
620                Err(
621                    UpdateParseError::new("expected DEFAULT, GRAPH <iri>, or <iri>", *pos)
622                        .with_location(ln, col),
623                )
624            }
625        }
626    }
627
628    // ── quad / triple data blocks ───────────────────────────────────────────
629
630    /// Parse QuadData per SPARQL 1.1 Update grammar:
631    ///   QuadData ::= '{' Quads '}'
632    ///   Quads    ::= TriplesTemplate? ( 'GRAPH' VarOrIri '{' TriplesTemplate? '}' '.'? TriplesTemplate? )*
633    ///
634    /// This handles both `{ triples }` (default graph) and
635    /// `{ GRAPH <iri> { triples } }` (named graph).
636    fn parse_quad_data(
637        &self,
638        input: &str,
639        pos: &mut usize,
640    ) -> Result<(Option<String>, Vec<TriplePattern>), UpdateParseError> {
641        *pos = self.skip_ws(input, *pos);
642
643        // Expect the outer opening brace of QuadData.
644        if *pos >= input.len() || input.as_bytes().get(*pos) != Some(&b'{') {
645            let (ln, col) = line_col(input, *pos);
646            return Err(
647                UpdateParseError::new("expected '{' to open quad data block", *pos)
648                    .with_location(ln, col),
649            );
650        }
651        *pos += 1; // consume outer '{'
652
653        *pos = self.skip_ws(input, *pos);
654
655        // Check whether the content starts with GRAPH keyword (named graph quad).
656        if self.match_keyword(input, *pos, "GRAPH") {
657            *pos = self.consume_keyword(input, *pos, "GRAPH")?;
658            *pos = self.skip_ws(input, *pos);
659            let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
660            *pos = new_pos;
661            *pos = self.skip_ws(input, *pos);
662
663            // Parse the inner brace block `{ triples }`.
664            let triples = self.parse_brace_block(input, pos)?;
665
666            // Consume optional trailing '.' after the GRAPH block.
667            *pos = self.skip_ws(input, *pos);
668            if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'.') {
669                *pos += 1;
670            }
671
672            // Consume outer closing brace.
673            *pos = self.skip_ws(input, *pos);
674            if *pos >= input.len() || input.as_bytes().get(*pos) != Some(&b'}') {
675                let (ln, col) = line_col(input, *pos);
676                return Err(
677                    UpdateParseError::new("expected '}' to close quad data block", *pos)
678                        .with_location(ln, col),
679                );
680            }
681            *pos += 1;
682
683            Ok((Some(iri), triples))
684        } else {
685            // Default graph triples: parse triples until the outer '}'.
686            let mut triples = Vec::new();
687            loop {
688                *pos = self.skip_ws(input, *pos);
689                if *pos >= input.len() {
690                    let (ln, col) = line_col(input, *pos);
691                    return Err(UpdateParseError::new(
692                        "unexpected end of input, expected '}'",
693                        *pos,
694                    )
695                    .with_location(ln, col));
696                }
697                if input.as_bytes().get(*pos) == Some(&b'}') {
698                    *pos += 1;
699                    break;
700                }
701
702                let triple = self.parse_triple_pattern(input, pos)?;
703                triples.push(triple);
704
705                *pos = self.skip_ws(input, *pos);
706                // Consume optional '.'
707                if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'.') {
708                    *pos += 1;
709                }
710            }
711
712            Ok((None, triples))
713        }
714    }
715
716    /// Parse `{ triple1 . triple2 . ... }`.
717    fn parse_brace_block(
718        &self,
719        input: &str,
720        pos: &mut usize,
721    ) -> Result<Vec<TriplePattern>, UpdateParseError> {
722        *pos = self.skip_ws(input, *pos);
723        if *pos >= input.len() || input.as_bytes().get(*pos) != Some(&b'{') {
724            let (ln, col) = line_col(input, *pos);
725            return Err(UpdateParseError::new("expected '{'", *pos).with_location(ln, col));
726        }
727        *pos += 1; // consume '{'
728
729        let mut triples = Vec::new();
730        loop {
731            *pos = self.skip_ws(input, *pos);
732            if *pos >= input.len() {
733                let (ln, col) = line_col(input, *pos);
734                return Err(
735                    UpdateParseError::new("unexpected end of input, expected '}'", *pos)
736                        .with_location(ln, col),
737                );
738            }
739            if input.as_bytes().get(*pos) == Some(&b'}') {
740                *pos += 1;
741                break;
742            }
743
744            let triple = self.parse_triple_pattern(input, pos)?;
745            triples.push(triple);
746
747            *pos = self.skip_ws(input, *pos);
748            // Consume optional '.'
749            if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'.') {
750                *pos += 1;
751            }
752        }
753
754        Ok(triples)
755    }
756
757    /// Parse a single triple pattern: subject predicate object.
758    fn parse_triple_pattern(
759        &self,
760        input: &str,
761        pos: &mut usize,
762    ) -> Result<TriplePattern, UpdateParseError> {
763        *pos = self.skip_ws(input, *pos);
764        let subject = self.read_term(input, pos)?;
765        *pos = self.skip_ws(input, *pos);
766        let predicate = self.read_term(input, pos)?;
767        *pos = self.skip_ws(input, *pos);
768        let object = self.read_term(input, pos)?;
769        Ok(TriplePattern::new(subject, predicate, object))
770    }
771
772    // ── low-level token readers ─────────────────────────────────────────────
773
774    /// Read a single term (IRI, prefixed name, variable, literal, blank node, or 'a').
775    fn read_term(&self, input: &str, pos: &mut usize) -> Result<String, UpdateParseError> {
776        *pos = self.skip_ws(input, *pos);
777        if *pos >= input.len() {
778            let (ln, col) = line_col(input, *pos);
779            return Err(
780                UpdateParseError::new("unexpected end of input while reading term", *pos)
781                    .with_location(ln, col),
782            );
783        }
784
785        let ch = input.as_bytes()[*pos];
786
787        // IRI reference
788        if ch == b'<' {
789            let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
790            *pos = new_pos;
791            return Ok(format!("<{}>", iri));
792        }
793
794        // Variable
795        if ch == b'?' || ch == b'$' {
796            let start = *pos;
797            *pos += 1; // skip ? or $
798            while *pos < input.len() && is_name_char(input.as_bytes()[*pos]) {
799                *pos += 1;
800            }
801            return Ok(input[start..*pos].to_string());
802        }
803
804        // Literal
805        if ch == b'"' || ch == b'\'' {
806            return self.read_literal(input, pos);
807        }
808
809        // Blank node
810        if ch == b'_' && input.as_bytes().get(*pos + 1) == Some(&b':') {
811            let start = *pos;
812            *pos += 2;
813            while *pos < input.len() && is_name_char(input.as_bytes()[*pos]) {
814                *pos += 1;
815            }
816            return Ok(input[start..*pos].to_string());
817        }
818
819        // Keyword 'a' (rdf:type shorthand)
820        if ch == b'a'
821            && (*pos + 1 >= input.len()
822                || !is_name_char(input.as_bytes().get(*pos + 1).copied().unwrap_or(b' ')))
823        {
824            *pos += 1;
825            return Ok("<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>".to_string());
826        }
827
828        // Prefixed name (prefix:local)
829        let start = *pos;
830        while *pos < input.len() && is_name_char(input.as_bytes()[*pos]) {
831            *pos += 1;
832        }
833        if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b':') {
834            let prefix = &input[start..*pos];
835            *pos += 1; // skip ':'
836            let local_start = *pos;
837            while *pos < input.len() && is_pname_local_char(input.as_bytes()[*pos]) {
838                *pos += 1;
839            }
840            let local = &input[local_start..*pos];
841
842            // Expand prefix if known
843            if let Some(ns) = self.prefixes.get(prefix) {
844                return Ok(format!("<{}{}>", ns, local));
845            }
846            return Ok(format!("{}:{}", prefix, local));
847        }
848
849        // Numeric literal
850        *pos = start; // reset
851        if ch.is_ascii_digit() || ch == b'+' || ch == b'-' {
852            return self.read_numeric_literal(input, pos);
853        }
854
855        // Boolean literals
856        if self.match_keyword(input, *pos, "true") {
857            *pos += 4;
858            return Ok("\"true\"^^<http://www.w3.org/2001/XMLSchema#boolean>".to_string());
859        }
860        if self.match_keyword(input, *pos, "false") {
861            *pos += 5;
862            return Ok("\"false\"^^<http://www.w3.org/2001/XMLSchema#boolean>".to_string());
863        }
864
865        let (ln, col) = line_col(input, *pos);
866        let snippet: String = input[*pos..].chars().take(20).collect();
867        Err(
868            UpdateParseError::new(format!("unexpected token: '{}'", snippet), *pos)
869                .with_location(ln, col),
870        )
871    }
872
873    /// Read a string literal (supports double-quoted and single-quoted).
874    fn read_literal(&self, input: &str, pos: &mut usize) -> Result<String, UpdateParseError> {
875        let quote = input.as_bytes()[*pos];
876        let start = *pos;
877        *pos += 1; // skip opening quote
878        let mut value = String::new();
879
880        while *pos < input.len() {
881            let ch = input.as_bytes()[*pos];
882            if ch == b'\\' && *pos + 1 < input.len() {
883                let esc = input.as_bytes()[*pos + 1];
884                let escaped = match esc {
885                    b'n' => '\n',
886                    b't' => '\t',
887                    b'\\' => '\\',
888                    b'"' => '"',
889                    b'\'' => '\'',
890                    _ => {
891                        *pos += 2;
892                        continue;
893                    }
894                };
895                value.push(escaped);
896                *pos += 2;
897            } else if ch == quote {
898                *pos += 1; // skip closing quote
899                           // Check for ^^<datatype> or @lang
900                if *pos < input.len()
901                    && input.as_bytes().get(*pos) == Some(&b'^')
902                    && input.as_bytes().get(*pos + 1) == Some(&b'^')
903                {
904                    *pos += 2;
905                    if input.as_bytes().get(*pos) == Some(&b'<') {
906                        let (dt, new_pos) = self.read_iri_ref(input, *pos)?;
907                        *pos = new_pos;
908                        return Ok(format!("\"{}\"^^<{}>", value, dt));
909                    }
910                }
911                if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'@') {
912                    *pos += 1;
913                    let lang_start = *pos;
914                    while *pos < input.len()
915                        && (input.as_bytes()[*pos].is_ascii_alphanumeric()
916                            || input.as_bytes()[*pos] == b'-')
917                    {
918                        *pos += 1;
919                    }
920                    let lang = &input[lang_start..*pos];
921                    return Ok(format!("\"{}\"@{}", value, lang));
922                }
923                return Ok(format!("\"{}\"", value));
924            } else {
925                value.push(ch as char);
926                *pos += 1;
927            }
928        }
929
930        let (ln, col) = line_col(input, start);
931        Err(UpdateParseError::new("unterminated string literal", start).with_location(ln, col))
932    }
933
934    /// Read a numeric literal (integer or decimal).
935    fn read_numeric_literal(
936        &self,
937        input: &str,
938        pos: &mut usize,
939    ) -> Result<String, UpdateParseError> {
940        let start = *pos;
941        // Optional sign
942        if *pos < input.len() && (input.as_bytes()[*pos] == b'+' || input.as_bytes()[*pos] == b'-')
943        {
944            *pos += 1;
945        }
946        // Digits
947        while *pos < input.len() && input.as_bytes()[*pos].is_ascii_digit() {
948            *pos += 1;
949        }
950        // Optional decimal part
951        let mut is_decimal = false;
952        if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'.') {
953            is_decimal = true;
954            *pos += 1;
955            while *pos < input.len() && input.as_bytes()[*pos].is_ascii_digit() {
956                *pos += 1;
957            }
958        }
959        // Optional exponent
960        if *pos < input.len() && (input.as_bytes()[*pos] == b'e' || input.as_bytes()[*pos] == b'E')
961        {
962            is_decimal = true;
963            *pos += 1;
964            if *pos < input.len()
965                && (input.as_bytes()[*pos] == b'+' || input.as_bytes()[*pos] == b'-')
966            {
967                *pos += 1;
968            }
969            while *pos < input.len() && input.as_bytes()[*pos].is_ascii_digit() {
970                *pos += 1;
971            }
972        }
973
974        if *pos == start {
975            let (ln, col) = line_col(input, *pos);
976            return Err(
977                UpdateParseError::new("expected numeric literal", *pos).with_location(ln, col)
978            );
979        }
980
981        let text = &input[start..*pos];
982        if is_decimal {
983            Ok(format!(
984                "\"{}\"^^<http://www.w3.org/2001/XMLSchema#double>",
985                text
986            ))
987        } else {
988            Ok(format!(
989                "\"{}\"^^<http://www.w3.org/2001/XMLSchema#integer>",
990                text
991            ))
992        }
993    }
994
995    /// Read an IRI enclosed in `< >`. Returns the IRI without angle brackets.
996    fn read_iri_ref(&self, input: &str, pos: usize) -> Result<(String, usize), UpdateParseError> {
997        if pos >= input.len() || input.as_bytes().get(pos) != Some(&b'<') {
998            let (ln, col) = line_col(input, pos);
999            return Err(
1000                UpdateParseError::new("expected '<' to start IRI reference", pos)
1001                    .with_location(ln, col),
1002            );
1003        }
1004        let start = pos + 1;
1005        let mut end = start;
1006        while end < input.len() && input.as_bytes()[end] != b'>' {
1007            end += 1;
1008        }
1009        if end >= input.len() {
1010            let (ln, col) = line_col(input, pos);
1011            return Err(
1012                UpdateParseError::new("unterminated IRI reference", pos).with_location(ln, col)
1013            );
1014        }
1015        let iri = input[start..end].to_string();
1016        Ok((iri, end + 1))
1017    }
1018
1019    /// Read a prefix label (e.g., `ex:` or `:`).
1020    fn read_prefix_label(
1021        &self,
1022        input: &str,
1023        pos: usize,
1024    ) -> Result<(String, usize), UpdateParseError> {
1025        let start = pos;
1026        let mut p = pos;
1027        while p < input.len() && input.as_bytes()[p] != b':' {
1028            p += 1;
1029        }
1030        if p >= input.len() {
1031            let (ln, col) = line_col(input, pos);
1032            return Err(
1033                UpdateParseError::new("expected ':' in prefix declaration", pos)
1034                    .with_location(ln, col),
1035            );
1036        }
1037        let prefix = input[start..p].trim().to_string();
1038        Ok((prefix, p + 1)) // skip ':'
1039    }
1040
1041    // ── utility helpers ─────────────────────────────────────────────────────
1042
1043    fn skip_ws(&self, input: &str, mut pos: usize) -> usize {
1044        let bytes = input.as_bytes();
1045        while pos < bytes.len() {
1046            if bytes[pos].is_ascii_whitespace() {
1047                pos += 1;
1048            } else if bytes[pos] == b'#' {
1049                // Skip line comment
1050                while pos < bytes.len() && bytes[pos] != b'\n' {
1051                    pos += 1;
1052                }
1053            } else {
1054                break;
1055            }
1056        }
1057        pos
1058    }
1059
1060    fn match_keyword(&self, input: &str, pos: usize, kw: &str) -> bool {
1061        let end = pos + kw.len();
1062        if end > input.len() {
1063            return false;
1064        }
1065        if !input[pos..end].eq_ignore_ascii_case(kw) {
1066            return false;
1067        }
1068        // Ensure keyword boundary
1069        end >= input.len() || !is_name_char(input.as_bytes()[end])
1070    }
1071
1072    fn consume_keyword(
1073        &self,
1074        input: &str,
1075        pos: usize,
1076        kw: &str,
1077    ) -> Result<usize, UpdateParseError> {
1078        if !self.match_keyword(input, pos, kw) {
1079            let (ln, col) = line_col(input, pos);
1080            let snippet: String = input[pos..].chars().take(20).collect();
1081            return Err(UpdateParseError::new(
1082                format!("expected keyword '{}', found: '{}'", kw, snippet),
1083                pos,
1084            )
1085            .with_location(ln, col));
1086        }
1087        Ok(pos + kw.len())
1088    }
1089
1090    /// Try to consume an optional keyword. Returns `true` if found.
1091    fn try_consume_keyword(&self, input: &str, pos: &mut usize, kw: &str) -> bool {
1092        if self.match_keyword(input, *pos, kw) {
1093            *pos += kw.len();
1094            true
1095        } else {
1096            false
1097        }
1098    }
1099}
1100
1101// ── character class helpers ─────────────────────────────────────────────────
1102
1103fn is_name_char(b: u8) -> bool {
1104    b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
1105}
1106
1107fn is_pname_local_char(b: u8) -> bool {
1108    b.is_ascii_alphanumeric() || b == b'_' || b == b'-' || b == b'.'
1109}
1110
1111// ────────────────────────────────────────────────────────────────────────────
1112// Convenience functions
1113// ────────────────────────────────────────────────────────────────────────────
1114
1115/// Parse a SPARQL Update request from a string.
1116pub fn parse_update(input: &str) -> Result<UpdateRequest, UpdateParseError> {
1117    let mut parser = UpdateParser::new();
1118    parser.parse(input)
1119}
1120
1121/// Parse a SPARQL Update request with pre-defined prefixes.
1122pub fn parse_update_with_prefixes(
1123    input: &str,
1124    prefixes: HashMap<String, String>,
1125) -> Result<UpdateRequest, UpdateParseError> {
1126    let mut parser = UpdateParser::with_prefixes(prefixes);
1127    parser.parse(input)
1128}
1129
1130// ────────────────────────────────────────────────────────────────────────────
1131// Tests
1132// ────────────────────────────────────────────────────────────────────────────
1133
1134#[cfg(test)]
1135mod tests {
1136    use super::*;
1137
1138    // ── INSERT DATA ─────────────────────────────────────────────────────────
1139
1140    #[test]
1141    fn test_insert_data_single_triple() {
1142        let input = r#"INSERT DATA { <http://ex.org/s> <http://ex.org/p> <http://ex.org/o> }"#;
1143        let req = parse_update(input).expect("should parse");
1144        assert_eq!(req.operations.len(), 1);
1145        match &req.operations[0] {
1146            UpdateOperation::InsertData { triples, graph } => {
1147                assert_eq!(triples.len(), 1);
1148                assert_eq!(triples[0].subject, "<http://ex.org/s>");
1149                assert_eq!(triples[0].predicate, "<http://ex.org/p>");
1150                assert_eq!(triples[0].object, "<http://ex.org/o>");
1151                assert!(graph.is_none());
1152            }
1153            other => panic!("expected InsertData, got {:?}", other),
1154        }
1155    }
1156
1157    #[test]
1158    fn test_insert_data_multiple_triples() {
1159        let input = r#"INSERT DATA {
1160            <http://ex.org/s1> <http://ex.org/p1> <http://ex.org/o1> .
1161            <http://ex.org/s2> <http://ex.org/p2> "hello"
1162        }"#;
1163        let req = parse_update(input).expect("should parse");
1164        assert_eq!(req.operations.len(), 1);
1165        if let UpdateOperation::InsertData { triples, .. } = &req.operations[0] {
1166            assert_eq!(triples.len(), 2);
1167            assert_eq!(triples[1].object, "\"hello\"");
1168        }
1169    }
1170
1171    #[test]
1172    fn test_insert_data_with_graph() {
1173        let input = r#"INSERT DATA { GRAPH <http://ex.org/g> { <http://ex.org/s> <http://ex.org/p> <http://ex.org/o> } }"#;
1174        let req = parse_update(input).expect("should parse");
1175        if let UpdateOperation::InsertData { graph, triples } = &req.operations[0] {
1176            assert_eq!(graph.as_deref(), Some("http://ex.org/g"));
1177            assert_eq!(triples.len(), 1);
1178        }
1179    }
1180
1181    #[test]
1182    fn test_insert_data_with_prefix() {
1183        let input = r#"PREFIX ex: <http://ex.org/>
1184        INSERT DATA { ex:s ex:p ex:o }"#;
1185        let req = parse_update(input).expect("should parse");
1186        if let UpdateOperation::InsertData { triples, .. } = &req.operations[0] {
1187            assert_eq!(triples[0].subject, "<http://ex.org/s>");
1188            assert_eq!(triples[0].predicate, "<http://ex.org/p>");
1189            assert_eq!(triples[0].object, "<http://ex.org/o>");
1190        }
1191    }
1192
1193    #[test]
1194    fn test_insert_data_with_literal_datatype() {
1195        let input = r#"INSERT DATA { <http://ex.org/s> <http://ex.org/p> "42"^^<http://www.w3.org/2001/XMLSchema#integer> }"#;
1196        let req = parse_update(input).expect("should parse");
1197        if let UpdateOperation::InsertData { triples, .. } = &req.operations[0] {
1198            assert!(triples[0].object.contains("integer"));
1199        }
1200    }
1201
1202    #[test]
1203    fn test_insert_data_with_lang_tag() {
1204        let input = r#"INSERT DATA { <http://ex.org/s> <http://ex.org/p> "bonjour"@fr }"#;
1205        let req = parse_update(input).expect("should parse");
1206        if let UpdateOperation::InsertData { triples, .. } = &req.operations[0] {
1207            assert_eq!(triples[0].object, "\"bonjour\"@fr");
1208        }
1209    }
1210
1211    // ── DELETE DATA ─────────────────────────────────────────────────────────
1212
1213    #[test]
1214    fn test_delete_data_single_triple() {
1215        let input = r#"DELETE DATA { <http://ex.org/s> <http://ex.org/p> <http://ex.org/o> }"#;
1216        let req = parse_update(input).expect("should parse");
1217        assert_eq!(req.operations.len(), 1);
1218        match &req.operations[0] {
1219            UpdateOperation::DeleteData { triples, graph } => {
1220                assert_eq!(triples.len(), 1);
1221                assert!(graph.is_none());
1222            }
1223            other => panic!("expected DeleteData, got {:?}", other),
1224        }
1225    }
1226
1227    #[test]
1228    fn test_delete_data_with_graph() {
1229        let input = r#"DELETE DATA { GRAPH <http://ex.org/g> { <http://ex.org/s> <http://ex.org/p> <http://ex.org/o> } }"#;
1230        let req = parse_update(input).expect("should parse");
1231        if let UpdateOperation::DeleteData { graph, .. } = &req.operations[0] {
1232            assert_eq!(graph.as_deref(), Some("http://ex.org/g"));
1233        }
1234    }
1235
1236    #[test]
1237    fn test_delete_data_multiple_triples() {
1238        let input = r#"DELETE DATA {
1239            <http://ex.org/s1> <http://ex.org/p1> <http://ex.org/o1> .
1240            <http://ex.org/s2> <http://ex.org/p2> <http://ex.org/o2>
1241        }"#;
1242        let req = parse_update(input).expect("should parse");
1243        if let UpdateOperation::DeleteData { triples, .. } = &req.operations[0] {
1244            assert_eq!(triples.len(), 2);
1245        }
1246    }
1247
1248    // ── DELETE/INSERT WHERE ─────────────────────────────────────────────────
1249
1250    #[test]
1251    fn test_delete_insert_where() {
1252        let input = r#"DELETE { ?s <http://ex.org/old> ?o }
1253        INSERT { ?s <http://ex.org/new> ?o }
1254        WHERE { ?s <http://ex.org/old> ?o }"#;
1255        let req = parse_update(input).expect("should parse");
1256        match &req.operations[0] {
1257            UpdateOperation::DeleteInsertWhere {
1258                delete_triples,
1259                insert_triples,
1260                where_triples,
1261                ..
1262            } => {
1263                assert_eq!(delete_triples.len(), 1);
1264                assert_eq!(insert_triples.len(), 1);
1265                assert_eq!(where_triples.len(), 1);
1266                assert_eq!(delete_triples[0].predicate, "<http://ex.org/old>");
1267                assert_eq!(insert_triples[0].predicate, "<http://ex.org/new>");
1268            }
1269            other => panic!("expected DeleteInsertWhere, got {:?}", other),
1270        }
1271    }
1272
1273    #[test]
1274    fn test_delete_where_only() {
1275        let input = r#"DELETE { ?s <http://ex.org/p> ?o }
1276        WHERE { ?s <http://ex.org/p> ?o }"#;
1277        let req = parse_update(input).expect("should parse");
1278        if let UpdateOperation::DeleteInsertWhere { insert_triples, .. } = &req.operations[0] {
1279            assert!(insert_triples.is_empty());
1280        }
1281    }
1282
1283    #[test]
1284    fn test_delete_insert_where_with_variables() {
1285        let input = r#"DELETE { ?person <http://ex.org/age> ?old }
1286        INSERT { ?person <http://ex.org/age> ?new }
1287        WHERE { ?person <http://ex.org/age> ?old }"#;
1288        let req = parse_update(input).expect("should parse");
1289        if let UpdateOperation::DeleteInsertWhere {
1290            delete_triples,
1291            insert_triples,
1292            ..
1293        } = &req.operations[0]
1294        {
1295            assert_eq!(delete_triples[0].subject, "?person");
1296            assert_eq!(insert_triples[0].object, "?new");
1297        }
1298    }
1299
1300    // ── LOAD ────────────────────────────────────────────────────────────────
1301
1302    #[test]
1303    fn test_load_basic() {
1304        let input = r#"LOAD <http://example.org/data.ttl>"#;
1305        let req = parse_update(input).expect("should parse");
1306        match &req.operations[0] {
1307            UpdateOperation::Load {
1308                source_uri,
1309                target_graph,
1310                silent,
1311            } => {
1312                assert_eq!(source_uri, "http://example.org/data.ttl");
1313                assert!(target_graph.is_none());
1314                assert!(!silent);
1315            }
1316            other => panic!("expected Load, got {:?}", other),
1317        }
1318    }
1319
1320    #[test]
1321    fn test_load_into_graph() {
1322        let input = r#"LOAD <http://example.org/data.ttl> INTO GRAPH <http://example.org/g>"#;
1323        let req = parse_update(input).expect("should parse");
1324        if let UpdateOperation::Load { target_graph, .. } = &req.operations[0] {
1325            assert_eq!(target_graph.as_deref(), Some("http://example.org/g"));
1326        }
1327    }
1328
1329    #[test]
1330    fn test_load_silent() {
1331        let input = r#"LOAD SILENT <http://example.org/data.ttl>"#;
1332        let req = parse_update(input).expect("should parse");
1333        if let UpdateOperation::Load { silent, .. } = &req.operations[0] {
1334            assert!(*silent);
1335        }
1336    }
1337
1338    #[test]
1339    fn test_load_silent_into_graph() {
1340        let input =
1341            r#"LOAD SILENT <http://example.org/data.ttl> INTO GRAPH <http://example.org/g>"#;
1342        let req = parse_update(input).expect("should parse");
1343        if let UpdateOperation::Load {
1344            silent,
1345            target_graph,
1346            ..
1347        } = &req.operations[0]
1348        {
1349            assert!(*silent);
1350            assert_eq!(target_graph.as_deref(), Some("http://example.org/g"));
1351        }
1352    }
1353
1354    // ── CLEAR ───────────────────────────────────────────────────────────────
1355
1356    #[test]
1357    fn test_clear_all() {
1358        let input = "CLEAR ALL";
1359        let req = parse_update(input).expect("should parse");
1360        match &req.operations[0] {
1361            UpdateOperation::Clear { target, silent } => {
1362                assert_eq!(*target, GraphTarget::All);
1363                assert!(!silent);
1364            }
1365            other => panic!("expected Clear, got {:?}", other),
1366        }
1367    }
1368
1369    #[test]
1370    fn test_clear_default() {
1371        let input = "CLEAR DEFAULT";
1372        let req = parse_update(input).expect("should parse");
1373        if let UpdateOperation::Clear { target, .. } = &req.operations[0] {
1374            assert_eq!(*target, GraphTarget::Default);
1375        }
1376    }
1377
1378    #[test]
1379    fn test_clear_named() {
1380        let input = "CLEAR NAMED";
1381        let req = parse_update(input).expect("should parse");
1382        if let UpdateOperation::Clear { target, .. } = &req.operations[0] {
1383            assert_eq!(*target, GraphTarget::Named);
1384        }
1385    }
1386
1387    #[test]
1388    fn test_clear_graph() {
1389        let input = "CLEAR GRAPH <http://example.org/g>";
1390        let req = parse_update(input).expect("should parse");
1391        if let UpdateOperation::Clear { target, .. } = &req.operations[0] {
1392            assert_eq!(
1393                *target,
1394                GraphTarget::Graph("http://example.org/g".to_string())
1395            );
1396        }
1397    }
1398
1399    #[test]
1400    fn test_clear_silent() {
1401        let input = "CLEAR SILENT ALL";
1402        let req = parse_update(input).expect("should parse");
1403        if let UpdateOperation::Clear { silent, .. } = &req.operations[0] {
1404            assert!(*silent);
1405        }
1406    }
1407
1408    // ── DROP ────────────────────────────────────────────────────────────────
1409
1410    #[test]
1411    fn test_drop_all() {
1412        let input = "DROP ALL";
1413        let req = parse_update(input).expect("should parse");
1414        match &req.operations[0] {
1415            UpdateOperation::Drop { target, silent } => {
1416                assert_eq!(*target, GraphTarget::All);
1417                assert!(!silent);
1418            }
1419            other => panic!("expected Drop, got {:?}", other),
1420        }
1421    }
1422
1423    #[test]
1424    fn test_drop_graph() {
1425        let input = "DROP GRAPH <http://ex.org/g>";
1426        let req = parse_update(input).expect("should parse");
1427        if let UpdateOperation::Drop { target, .. } = &req.operations[0] {
1428            assert_eq!(*target, GraphTarget::Graph("http://ex.org/g".to_string()));
1429        }
1430    }
1431
1432    #[test]
1433    fn test_drop_silent() {
1434        let input = "DROP SILENT DEFAULT";
1435        let req = parse_update(input).expect("should parse");
1436        if let UpdateOperation::Drop { target, silent } = &req.operations[0] {
1437            assert_eq!(*target, GraphTarget::Default);
1438            assert!(*silent);
1439        }
1440    }
1441
1442    // ── CREATE GRAPH ────────────────────────────────────────────────────────
1443
1444    #[test]
1445    fn test_create_graph() {
1446        let input = "CREATE GRAPH <http://example.org/new-graph>";
1447        let req = parse_update(input).expect("should parse");
1448        match &req.operations[0] {
1449            UpdateOperation::CreateGraph { graph_iri, silent } => {
1450                assert_eq!(graph_iri, "http://example.org/new-graph");
1451                assert!(!silent);
1452            }
1453            other => panic!("expected CreateGraph, got {:?}", other),
1454        }
1455    }
1456
1457    #[test]
1458    fn test_create_graph_silent() {
1459        let input = "CREATE SILENT GRAPH <http://example.org/g>";
1460        let req = parse_update(input).expect("should parse");
1461        if let UpdateOperation::CreateGraph { silent, .. } = &req.operations[0] {
1462            assert!(*silent);
1463        }
1464    }
1465
1466    // ── COPY ────────────────────────────────────────────────────────────────
1467
1468    #[test]
1469    fn test_copy_default_to_graph() {
1470        let input = "COPY DEFAULT TO GRAPH <http://ex.org/backup>";
1471        let req = parse_update(input).expect("should parse");
1472        match &req.operations[0] {
1473            UpdateOperation::Copy {
1474                source,
1475                destination,
1476                silent,
1477            } => {
1478                assert_eq!(*source, GraphTarget::Default);
1479                assert_eq!(
1480                    *destination,
1481                    GraphTarget::Graph("http://ex.org/backup".to_string())
1482                );
1483                assert!(!silent);
1484            }
1485            other => panic!("expected Copy, got {:?}", other),
1486        }
1487    }
1488
1489    #[test]
1490    fn test_copy_graph_to_default() {
1491        let input = "COPY GRAPH <http://ex.org/src> TO DEFAULT";
1492        let req = parse_update(input).expect("should parse");
1493        if let UpdateOperation::Copy {
1494            source,
1495            destination,
1496            ..
1497        } = &req.operations[0]
1498        {
1499            assert_eq!(*source, GraphTarget::Graph("http://ex.org/src".to_string()));
1500            assert_eq!(*destination, GraphTarget::Default);
1501        }
1502    }
1503
1504    #[test]
1505    fn test_copy_silent() {
1506        let input = "COPY SILENT DEFAULT TO GRAPH <http://ex.org/dst>";
1507        let req = parse_update(input).expect("should parse");
1508        if let UpdateOperation::Copy { silent, .. } = &req.operations[0] {
1509            assert!(*silent);
1510        }
1511    }
1512
1513    // ── MOVE ────────────────────────────────────────────────────────────────
1514
1515    #[test]
1516    fn test_move_graph_to_graph() {
1517        let input = "MOVE GRAPH <http://ex.org/a> TO GRAPH <http://ex.org/b>";
1518        let req = parse_update(input).expect("should parse");
1519        match &req.operations[0] {
1520            UpdateOperation::Move {
1521                source,
1522                destination,
1523                silent,
1524            } => {
1525                assert_eq!(*source, GraphTarget::Graph("http://ex.org/a".to_string()));
1526                assert_eq!(
1527                    *destination,
1528                    GraphTarget::Graph("http://ex.org/b".to_string())
1529                );
1530                assert!(!silent);
1531            }
1532            other => panic!("expected Move, got {:?}", other),
1533        }
1534    }
1535
1536    #[test]
1537    fn test_move_silent() {
1538        let input = "MOVE SILENT GRAPH <http://ex.org/a> TO DEFAULT";
1539        let req = parse_update(input).expect("should parse");
1540        if let UpdateOperation::Move { silent, .. } = &req.operations[0] {
1541            assert!(*silent);
1542        }
1543    }
1544
1545    // ── ADD ─────────────────────────────────────────────────────────────────
1546
1547    #[test]
1548    fn test_add_default_to_graph() {
1549        let input = "ADD DEFAULT TO GRAPH <http://ex.org/combined>";
1550        let req = parse_update(input).expect("should parse");
1551        match &req.operations[0] {
1552            UpdateOperation::Add {
1553                source,
1554                destination,
1555                silent,
1556            } => {
1557                assert_eq!(*source, GraphTarget::Default);
1558                assert_eq!(
1559                    *destination,
1560                    GraphTarget::Graph("http://ex.org/combined".to_string())
1561                );
1562                assert!(!silent);
1563            }
1564            other => panic!("expected Add, got {:?}", other),
1565        }
1566    }
1567
1568    #[test]
1569    fn test_add_silent() {
1570        let input = "ADD SILENT GRAPH <http://ex.org/src> TO DEFAULT";
1571        let req = parse_update(input).expect("should parse");
1572        if let UpdateOperation::Add { silent, .. } = &req.operations[0] {
1573            assert!(*silent);
1574        }
1575    }
1576
1577    // ── Multiple operations ─────────────────────────────────────────────────
1578
1579    #[test]
1580    fn test_multiple_operations_semicolon_separated() {
1581        let input = r#"INSERT DATA { <http://ex.org/s> <http://ex.org/p> <http://ex.org/o> } ;
1582        CLEAR ALL"#;
1583        let req = parse_update(input).expect("should parse");
1584        assert_eq!(req.operations.len(), 2);
1585        assert_eq!(req.operations[0].kind_label(), "INSERT DATA");
1586        assert_eq!(req.operations[1].kind_label(), "CLEAR");
1587    }
1588
1589    #[test]
1590    fn test_three_operations() {
1591        let input = r#"
1592            CREATE GRAPH <http://ex.org/g> ;
1593            LOAD <http://ex.org/data.ttl> INTO GRAPH <http://ex.org/g> ;
1594            DROP GRAPH <http://ex.org/old>
1595        "#;
1596        let req = parse_update(input).expect("should parse");
1597        assert_eq!(req.operations.len(), 3);
1598        assert_eq!(req.operations[0].kind_label(), "CREATE GRAPH");
1599        assert_eq!(req.operations[1].kind_label(), "LOAD");
1600        assert_eq!(req.operations[2].kind_label(), "DROP");
1601    }
1602
1603    // ── PREFIX handling ─────────────────────────────────────────────────────
1604
1605    #[test]
1606    fn test_multiple_prefixes() {
1607        let input = r#"
1608            PREFIX ex: <http://example.org/>
1609            PREFIX foaf: <http://xmlns.com/foaf/0.1/>
1610            INSERT DATA { ex:alice foaf:name "Alice" }
1611        "#;
1612        let req = parse_update(input).expect("should parse");
1613        assert_eq!(req.prefixes.len(), 2);
1614        assert_eq!(
1615            req.prefixes.get("ex"),
1616            Some(&"http://example.org/".to_string())
1617        );
1618        assert_eq!(
1619            req.prefixes.get("foaf"),
1620            Some(&"http://xmlns.com/foaf/0.1/".to_string())
1621        );
1622    }
1623
1624    #[test]
1625    fn test_prefix_expansion_in_triples() {
1626        let input = r#"
1627            PREFIX foaf: <http://xmlns.com/foaf/0.1/>
1628            INSERT DATA { <http://ex.org/alice> foaf:knows <http://ex.org/bob> }
1629        "#;
1630        let req = parse_update(input).expect("should parse");
1631        if let UpdateOperation::InsertData { triples, .. } = &req.operations[0] {
1632            assert_eq!(triples[0].predicate, "<http://xmlns.com/foaf/0.1/knows>");
1633        }
1634    }
1635
1636    // ── Error cases ─────────────────────────────────────────────────────────
1637
1638    #[test]
1639    fn test_error_empty_input() {
1640        let result = parse_update("");
1641        assert!(result.is_err());
1642        let err = result.expect_err("should be error");
1643        assert!(err.message.contains("empty"));
1644    }
1645
1646    #[test]
1647    fn test_error_unknown_keyword() {
1648        let result = parse_update("FROBNICATE ALL");
1649        assert!(result.is_err());
1650        let err = result.expect_err("should be error");
1651        assert!(err.message.contains("expected update keyword"));
1652        assert!(err.position == 0);
1653    }
1654
1655    #[test]
1656    fn test_error_missing_brace() {
1657        let result =
1658            parse_update("INSERT DATA <http://ex.org/s> <http://ex.org/p> <http://ex.org/o>");
1659        assert!(result.is_err());
1660    }
1661
1662    #[test]
1663    fn test_error_unterminated_brace() {
1664        let result =
1665            parse_update("INSERT DATA { <http://ex.org/s> <http://ex.org/p> <http://ex.org/o>");
1666        assert!(result.is_err());
1667        let err = result.expect_err("should be error");
1668        assert!(err.message.contains("'}'"));
1669    }
1670
1671    #[test]
1672    fn test_error_position_tracking() {
1673        let result = parse_update("CLEAR BADTARGET");
1674        assert!(result.is_err());
1675        let err = result.expect_err("should be error");
1676        assert!(err.position > 0);
1677        assert!(err.line.is_some());
1678        assert!(err.column.is_some());
1679    }
1680
1681    #[test]
1682    fn test_error_unterminated_iri() {
1683        let result = parse_update("LOAD <http://example.org/unterminated");
1684        assert!(result.is_err());
1685        let err = result.expect_err("should be error");
1686        assert!(err.message.contains("unterminated"));
1687    }
1688
1689    // ── Special terms ───────────────────────────────────────────────────────
1690
1691    #[test]
1692    fn test_rdf_type_shorthand() {
1693        let input = r#"INSERT DATA { <http://ex.org/alice> a <http://ex.org/Person> }"#;
1694        let req = parse_update(input).expect("should parse");
1695        if let UpdateOperation::InsertData { triples, .. } = &req.operations[0] {
1696            assert_eq!(
1697                triples[0].predicate,
1698                "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
1699            );
1700        }
1701    }
1702
1703    #[test]
1704    fn test_blank_node() {
1705        let input = r#"INSERT DATA { _:b1 <http://ex.org/p> <http://ex.org/o> }"#;
1706        let req = parse_update(input).expect("should parse");
1707        if let UpdateOperation::InsertData { triples, .. } = &req.operations[0] {
1708            assert_eq!(triples[0].subject, "_:b1");
1709        }
1710    }
1711
1712    #[test]
1713    fn test_numeric_literal_integer() {
1714        let input = r#"INSERT DATA { <http://ex.org/s> <http://ex.org/age> 42 }"#;
1715        let req = parse_update(input).expect("should parse");
1716        if let UpdateOperation::InsertData { triples, .. } = &req.operations[0] {
1717            assert!(triples[0].object.contains("42"));
1718            assert!(triples[0].object.contains("integer"));
1719        }
1720    }
1721
1722    #[test]
1723    fn test_numeric_literal_decimal() {
1724        let input = r#"INSERT DATA { <http://ex.org/s> <http://ex.org/weight> 3.14 }"#;
1725        let req = parse_update(input).expect("should parse");
1726        if let UpdateOperation::InsertData { triples, .. } = &req.operations[0] {
1727            assert!(triples[0].object.contains("3.14"));
1728        }
1729    }
1730
1731    // ── GraphTarget display ─────────────────────────────────────────────────
1732
1733    #[test]
1734    fn test_graph_target_display() {
1735        assert_eq!(GraphTarget::Default.to_string(), "DEFAULT");
1736        assert_eq!(GraphTarget::Named.to_string(), "NAMED");
1737        assert_eq!(GraphTarget::All.to_string(), "ALL");
1738        assert_eq!(
1739            GraphTarget::Graph("http://ex.org/g".to_string()).to_string(),
1740            "GRAPH <http://ex.org/g>"
1741        );
1742    }
1743
1744    // ── UpdateParseError display ────────────────────────────────────────────
1745
1746    #[test]
1747    fn test_error_display_with_location() {
1748        let err = UpdateParseError::new("bad token", 10).with_location(2, 5);
1749        let msg = err.to_string();
1750        assert!(msg.contains("2:5"));
1751        assert!(msg.contains("bad token"));
1752    }
1753
1754    #[test]
1755    fn test_error_display_without_location() {
1756        let err = UpdateParseError::new("bad token", 10);
1757        let msg = err.to_string();
1758        assert!(msg.contains("byte 10"));
1759        assert!(msg.contains("bad token"));
1760    }
1761
1762    // ── Kind label ──────────────────────────────────────────────────────────
1763
1764    #[test]
1765    fn test_kind_labels() {
1766        assert_eq!(
1767            UpdateOperation::InsertData {
1768                triples: vec![],
1769                graph: None
1770            }
1771            .kind_label(),
1772            "INSERT DATA"
1773        );
1774        assert_eq!(
1775            UpdateOperation::DeleteData {
1776                triples: vec![],
1777                graph: None
1778            }
1779            .kind_label(),
1780            "DELETE DATA"
1781        );
1782        assert_eq!(
1783            UpdateOperation::Load {
1784                source_uri: String::new(),
1785                target_graph: None,
1786                silent: false
1787            }
1788            .kind_label(),
1789            "LOAD"
1790        );
1791        assert_eq!(
1792            UpdateOperation::Clear {
1793                target: GraphTarget::All,
1794                silent: false
1795            }
1796            .kind_label(),
1797            "CLEAR"
1798        );
1799        assert_eq!(
1800            UpdateOperation::Drop {
1801                target: GraphTarget::All,
1802                silent: false
1803            }
1804            .kind_label(),
1805            "DROP"
1806        );
1807        assert_eq!(
1808            UpdateOperation::CreateGraph {
1809                graph_iri: String::new(),
1810                silent: false
1811            }
1812            .kind_label(),
1813            "CREATE GRAPH"
1814        );
1815        assert_eq!(
1816            UpdateOperation::Copy {
1817                source: GraphTarget::Default,
1818                destination: GraphTarget::Default,
1819                silent: false
1820            }
1821            .kind_label(),
1822            "COPY"
1823        );
1824        assert_eq!(
1825            UpdateOperation::Move {
1826                source: GraphTarget::Default,
1827                destination: GraphTarget::Default,
1828                silent: false
1829            }
1830            .kind_label(),
1831            "MOVE"
1832        );
1833        assert_eq!(
1834            UpdateOperation::Add {
1835                source: GraphTarget::Default,
1836                destination: GraphTarget::Default,
1837                silent: false
1838            }
1839            .kind_label(),
1840            "ADD"
1841        );
1842    }
1843
1844    // ── Comment handling ────────────────────────────────────────────────────
1845
1846    #[test]
1847    fn test_comments_are_skipped() {
1848        let input = r#"
1849            # This is a comment
1850            INSERT DATA {
1851                # Another comment
1852                <http://ex.org/s> <http://ex.org/p> <http://ex.org/o>
1853            }
1854        "#;
1855        let req = parse_update(input).expect("should parse");
1856        assert_eq!(req.operations.len(), 1);
1857    }
1858
1859    // ── with_prefixes constructor ───────────────────────────────────────────
1860
1861    #[test]
1862    fn test_with_prefixes_constructor() {
1863        let mut prefixes = HashMap::new();
1864        prefixes.insert("ex".to_string(), "http://example.org/".to_string());
1865
1866        let input = "INSERT DATA { ex:s ex:p ex:o }";
1867        let result = parse_update_with_prefixes(input, prefixes);
1868        let req = result.expect("should parse");
1869        if let UpdateOperation::InsertData { triples, .. } = &req.operations[0] {
1870            assert_eq!(triples[0].subject, "<http://example.org/s>");
1871        }
1872    }
1873
1874    // ── Triple pattern construction ─────────────────────────────────────────
1875
1876    #[test]
1877    fn test_triple_pattern_new() {
1878        let tp = TriplePattern::new("s", "p", "o");
1879        assert_eq!(tp.subject, "s");
1880        assert_eq!(tp.predicate, "p");
1881        assert_eq!(tp.object, "o");
1882    }
1883
1884    // ── Case insensitivity ──────────────────────────────────────────────────
1885
1886    #[test]
1887    fn test_keywords_case_insensitive() {
1888        let input = "clear all";
1889        let req = parse_update(input).expect("should parse");
1890        assert_eq!(req.operations[0].kind_label(), "CLEAR");
1891    }
1892
1893    #[test]
1894    fn test_mixed_case_keywords() {
1895        let input = "Insert Data { <http://ex.org/s> <http://ex.org/p> <http://ex.org/o> }";
1896        let req = parse_update(input).expect("should parse");
1897        assert_eq!(req.operations[0].kind_label(), "INSERT DATA");
1898    }
1899
1900    // ── line_col helper ─────────────────────────────────────────────────────
1901
1902    #[test]
1903    fn test_line_col_first_line() {
1904        let (ln, col) = line_col("hello world", 6);
1905        assert_eq!(ln, 1);
1906        assert_eq!(col, 7);
1907    }
1908
1909    #[test]
1910    fn test_line_col_second_line() {
1911        let (ln, col) = line_col("hello\nworld", 6);
1912        assert_eq!(ln, 2);
1913        assert_eq!(col, 1);
1914    }
1915
1916    #[test]
1917    fn test_line_col_empty() {
1918        let (ln, col) = line_col("", 0);
1919        assert_eq!(ln, 1);
1920        assert_eq!(col, 1);
1921    }
1922
1923    // ── UpdateParser default ────────────────────────────────────────────────
1924
1925    #[test]
1926    fn test_parser_default_trait() {
1927        let parser = UpdateParser::default();
1928        assert!(parser.prefixes.is_empty());
1929    }
1930}