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.
64pub(crate) fn 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    /// Returns the parser's current namespace prefix map.
235    pub fn prefixes(&self) -> &HashMap<String, String> {
236        &self.prefixes
237    }
238
239    /// Parse a full SPARQL Update request string.
240    pub fn parse(&mut self, input: &str) -> Result<UpdateRequest, UpdateParseError> {
241        let mut pos = 0usize;
242        let mut operations = Vec::new();
243
244        // Parse prologue (BASE / PREFIX declarations).
245        pos = self.skip_ws(input, pos);
246        pos = self.parse_prologue(input, pos)?;
247
248        // Parse one or more update operations separated by `;`.
249        loop {
250            pos = self.skip_ws(input, pos);
251            if pos >= input.len() {
252                break;
253            }
254
255            let op = self.parse_operation(input, &mut pos)?;
256            operations.push(op);
257
258            pos = self.skip_ws(input, pos);
259            if pos < input.len() && input.as_bytes().get(pos) == Some(&b';') {
260                pos += 1; // consume separator
261            }
262        }
263
264        if operations.is_empty() {
265            let (ln, col) = line_col(input, pos);
266            return Err(UpdateParseError::new("empty update request", pos).with_location(ln, col));
267        }
268
269        Ok(UpdateRequest {
270            prefixes: self.prefixes.clone(),
271            operations,
272        })
273    }
274
275    // ── prologue ────────────────────────────────────────────────────────────
276
277    fn parse_prologue(&mut self, input: &str, mut pos: usize) -> Result<usize, UpdateParseError> {
278        loop {
279            pos = self.skip_ws(input, pos);
280            if self.match_keyword(input, pos, "PREFIX") {
281                pos = self.consume_keyword(input, pos, "PREFIX")?;
282                pos = self.skip_ws(input, pos);
283
284                let (prefix, new_pos) = self.read_prefix_label(input, pos)?;
285                pos = self.skip_ws(input, new_pos);
286
287                let (iri, new_pos) = self.read_iri_ref(input, pos)?;
288                pos = new_pos;
289
290                self.prefixes.insert(prefix, iri);
291            } else if self.match_keyword(input, pos, "BASE") {
292                pos = self.consume_keyword(input, pos, "BASE")?;
293                pos = self.skip_ws(input, pos);
294                // Read the IRI but we don't use BASE resolution in this simple parser.
295                let (_, new_pos) = self.read_iri_ref(input, pos)?;
296                pos = new_pos;
297            } else {
298                break;
299            }
300        }
301        Ok(pos)
302    }
303
304    // ── operation dispatch ──────────────────────────────────────────────────
305
306    fn parse_operation(
307        &self,
308        input: &str,
309        pos: &mut usize,
310    ) -> Result<UpdateOperation, UpdateParseError> {
311        *pos = self.skip_ws(input, *pos);
312        if *pos >= input.len() {
313            let (ln, col) = line_col(input, *pos);
314            return Err(
315                UpdateParseError::new("unexpected end of input", *pos).with_location(ln, col)
316            );
317        }
318
319        // Determine which operation keyword is at the current position.
320        if self.match_keyword(input, *pos, "INSERT") {
321            self.parse_insert(input, pos)
322        } else if self.match_keyword(input, *pos, "DELETE") {
323            self.parse_delete(input, pos)
324        } else if self.match_keyword(input, *pos, "LOAD") {
325            self.parse_load(input, pos)
326        } else if self.match_keyword(input, *pos, "CLEAR") {
327            self.parse_clear(input, pos)
328        } else if self.match_keyword(input, *pos, "DROP") {
329            self.parse_drop(input, pos)
330        } else if self.match_keyword(input, *pos, "CREATE") {
331            self.parse_create(input, pos)
332        } else if self.match_keyword(input, *pos, "COPY") {
333            self.parse_copy(input, pos)
334        } else if self.match_keyword(input, *pos, "MOVE") {
335            self.parse_move(input, pos)
336        } else if self.match_keyword(input, *pos, "ADD") {
337            self.parse_add(input, pos)
338        } else {
339            let (ln, col) = line_col(input, *pos);
340            let snippet: String = input[*pos..].chars().take(20).collect();
341            Err(UpdateParseError::new(
342                format!("expected update keyword, found: '{}'", snippet),
343                *pos,
344            )
345            .with_location(ln, col))
346        }
347    }
348
349    // ── INSERT ──────────────────────────────────────────────────────────────
350
351    fn parse_insert(
352        &self,
353        input: &str,
354        pos: &mut usize,
355    ) -> Result<UpdateOperation, UpdateParseError> {
356        *pos = self.consume_keyword(input, *pos, "INSERT")?;
357        *pos = self.skip_ws(input, *pos);
358
359        if self.match_keyword(input, *pos, "DATA") {
360            *pos = self.consume_keyword(input, *pos, "DATA")?;
361            *pos = self.skip_ws(input, *pos);
362
363            let (graph, triples) = self.parse_quad_data(input, pos)?;
364            Ok(UpdateOperation::InsertData { triples, graph })
365        } else {
366            // INSERT { ... } WHERE { ... }   (only insert part of delete/insert)
367            let (ln, col) = line_col(input, *pos);
368            Err(UpdateParseError::new(
369                "expected DATA after INSERT (standalone INSERT without DELETE is not supported here; use DELETE/INSERT WHERE)",
370                *pos,
371            )
372            .with_location(ln, col))
373        }
374    }
375
376    // ── DELETE ──────────────────────────────────────────────────────────────
377
378    fn parse_delete(
379        &self,
380        input: &str,
381        pos: &mut usize,
382    ) -> Result<UpdateOperation, UpdateParseError> {
383        *pos = self.consume_keyword(input, *pos, "DELETE")?;
384        *pos = self.skip_ws(input, *pos);
385
386        if self.match_keyword(input, *pos, "DATA") {
387            *pos = self.consume_keyword(input, *pos, "DATA")?;
388            *pos = self.skip_ws(input, *pos);
389
390            let (graph, triples) = self.parse_quad_data(input, pos)?;
391            Ok(UpdateOperation::DeleteData { triples, graph })
392        } else if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'{') {
393            // DELETE { ... } INSERT { ... } WHERE { ... }
394            let delete_triples = self.parse_brace_block(input, pos)?;
395            *pos = self.skip_ws(input, *pos);
396
397            let mut insert_triples = Vec::new();
398            if self.match_keyword(input, *pos, "INSERT") {
399                *pos = self.consume_keyword(input, *pos, "INSERT")?;
400                *pos = self.skip_ws(input, *pos);
401                insert_triples = self.parse_brace_block(input, pos)?;
402                *pos = self.skip_ws(input, *pos);
403            }
404
405            *pos = self.consume_keyword(input, *pos, "WHERE")?;
406            *pos = self.skip_ws(input, *pos);
407            let where_triples = self.parse_brace_block(input, pos)?;
408
409            Ok(UpdateOperation::DeleteInsertWhere {
410                delete_triples,
411                insert_triples,
412                where_triples,
413                graph: None,
414            })
415        } else {
416            let (ln, col) = line_col(input, *pos);
417            Err(
418                UpdateParseError::new("expected DATA or '{' after DELETE", *pos)
419                    .with_location(ln, col),
420            )
421        }
422    }
423
424    // ── LOAD ────────────────────────────────────────────────────────────────
425
426    fn parse_load(
427        &self,
428        input: &str,
429        pos: &mut usize,
430    ) -> Result<UpdateOperation, UpdateParseError> {
431        *pos = self.consume_keyword(input, *pos, "LOAD")?;
432        *pos = self.skip_ws(input, *pos);
433
434        let silent = self.try_consume_keyword(input, pos, "SILENT");
435        *pos = self.skip_ws(input, *pos);
436
437        let (source_uri, new_pos) = self.read_iri_ref(input, *pos)?;
438        *pos = new_pos;
439        *pos = self.skip_ws(input, *pos);
440
441        let target_graph = if self.match_keyword(input, *pos, "INTO") {
442            *pos = self.consume_keyword(input, *pos, "INTO")?;
443            *pos = self.skip_ws(input, *pos);
444            *pos = self.consume_keyword(input, *pos, "GRAPH")?;
445            *pos = self.skip_ws(input, *pos);
446            let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
447            *pos = new_pos;
448            Some(iri)
449        } else {
450            None
451        };
452
453        Ok(UpdateOperation::Load {
454            source_uri,
455            target_graph,
456            silent,
457        })
458    }
459
460    // ── CLEAR / DROP ────────────────────────────────────────────────────────
461
462    fn parse_clear(
463        &self,
464        input: &str,
465        pos: &mut usize,
466    ) -> Result<UpdateOperation, UpdateParseError> {
467        *pos = self.consume_keyword(input, *pos, "CLEAR")?;
468        *pos = self.skip_ws(input, *pos);
469        let silent = self.try_consume_keyword(input, pos, "SILENT");
470        *pos = self.skip_ws(input, *pos);
471        let target = self.parse_graph_ref_all(input, pos)?;
472        Ok(UpdateOperation::Clear { target, silent })
473    }
474
475    fn parse_drop(
476        &self,
477        input: &str,
478        pos: &mut usize,
479    ) -> Result<UpdateOperation, UpdateParseError> {
480        *pos = self.consume_keyword(input, *pos, "DROP")?;
481        *pos = self.skip_ws(input, *pos);
482        let silent = self.try_consume_keyword(input, pos, "SILENT");
483        *pos = self.skip_ws(input, *pos);
484        let target = self.parse_graph_ref_all(input, pos)?;
485        Ok(UpdateOperation::Drop { target, silent })
486    }
487
488    // ── CREATE ──────────────────────────────────────────────────────────────
489
490    fn parse_create(
491        &self,
492        input: &str,
493        pos: &mut usize,
494    ) -> Result<UpdateOperation, UpdateParseError> {
495        *pos = self.consume_keyword(input, *pos, "CREATE")?;
496        *pos = self.skip_ws(input, *pos);
497        let silent = self.try_consume_keyword(input, pos, "SILENT");
498        *pos = self.skip_ws(input, *pos);
499        *pos = self.consume_keyword(input, *pos, "GRAPH")?;
500        *pos = self.skip_ws(input, *pos);
501        let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
502        *pos = new_pos;
503        Ok(UpdateOperation::CreateGraph {
504            graph_iri: iri,
505            silent,
506        })
507    }
508
509    // ── COPY / MOVE / ADD ───────────────────────────────────────────────────
510
511    fn parse_copy(
512        &self,
513        input: &str,
514        pos: &mut usize,
515    ) -> Result<UpdateOperation, UpdateParseError> {
516        *pos = self.consume_keyword(input, *pos, "COPY")?;
517        *pos = self.skip_ws(input, *pos);
518        let silent = self.try_consume_keyword(input, pos, "SILENT");
519        *pos = self.skip_ws(input, *pos);
520        let source = self.parse_graph_or_default(input, pos)?;
521        *pos = self.skip_ws(input, *pos);
522        *pos = self.consume_keyword(input, *pos, "TO")?;
523        *pos = self.skip_ws(input, *pos);
524        let destination = self.parse_graph_or_default(input, pos)?;
525        Ok(UpdateOperation::Copy {
526            source,
527            destination,
528            silent,
529        })
530    }
531
532    fn parse_move(
533        &self,
534        input: &str,
535        pos: &mut usize,
536    ) -> Result<UpdateOperation, UpdateParseError> {
537        *pos = self.consume_keyword(input, *pos, "MOVE")?;
538        *pos = self.skip_ws(input, *pos);
539        let silent = self.try_consume_keyword(input, pos, "SILENT");
540        *pos = self.skip_ws(input, *pos);
541        let source = self.parse_graph_or_default(input, pos)?;
542        *pos = self.skip_ws(input, *pos);
543        *pos = self.consume_keyword(input, *pos, "TO")?;
544        *pos = self.skip_ws(input, *pos);
545        let destination = self.parse_graph_or_default(input, pos)?;
546        Ok(UpdateOperation::Move {
547            source,
548            destination,
549            silent,
550        })
551    }
552
553    fn parse_add(&self, input: &str, pos: &mut usize) -> Result<UpdateOperation, UpdateParseError> {
554        *pos = self.consume_keyword(input, *pos, "ADD")?;
555        *pos = self.skip_ws(input, *pos);
556        let silent = self.try_consume_keyword(input, pos, "SILENT");
557        *pos = self.skip_ws(input, *pos);
558        let source = self.parse_graph_or_default(input, pos)?;
559        *pos = self.skip_ws(input, *pos);
560        *pos = self.consume_keyword(input, *pos, "TO")?;
561        *pos = self.skip_ws(input, *pos);
562        let destination = self.parse_graph_or_default(input, pos)?;
563        Ok(UpdateOperation::Add {
564            source,
565            destination,
566            silent,
567        })
568    }
569
570    // ── graph references ────────────────────────────────────────────────────
571
572    fn parse_graph_ref_all(
573        &self,
574        input: &str,
575        pos: &mut usize,
576    ) -> Result<GraphTarget, UpdateParseError> {
577        *pos = self.skip_ws(input, *pos);
578        if self.match_keyword(input, *pos, "ALL") {
579            *pos = self.consume_keyword(input, *pos, "ALL")?;
580            Ok(GraphTarget::All)
581        } else if self.match_keyword(input, *pos, "DEFAULT") {
582            *pos = self.consume_keyword(input, *pos, "DEFAULT")?;
583            Ok(GraphTarget::Default)
584        } else if self.match_keyword(input, *pos, "NAMED") {
585            *pos = self.consume_keyword(input, *pos, "NAMED")?;
586            Ok(GraphTarget::Named)
587        } else if self.match_keyword(input, *pos, "GRAPH") {
588            *pos = self.consume_keyword(input, *pos, "GRAPH")?;
589            *pos = self.skip_ws(input, *pos);
590            let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
591            *pos = new_pos;
592            Ok(GraphTarget::Graph(iri))
593        } else {
594            let (ln, col) = line_col(input, *pos);
595            Err(
596                UpdateParseError::new("expected GRAPH, DEFAULT, NAMED, or ALL", *pos)
597                    .with_location(ln, col),
598            )
599        }
600    }
601
602    fn parse_graph_or_default(
603        &self,
604        input: &str,
605        pos: &mut usize,
606    ) -> Result<GraphTarget, UpdateParseError> {
607        *pos = self.skip_ws(input, *pos);
608        if self.match_keyword(input, *pos, "DEFAULT") {
609            *pos = self.consume_keyword(input, *pos, "DEFAULT")?;
610            Ok(GraphTarget::Default)
611        } else if self.match_keyword(input, *pos, "GRAPH") {
612            *pos = self.consume_keyword(input, *pos, "GRAPH")?;
613            *pos = self.skip_ws(input, *pos);
614            let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
615            *pos = new_pos;
616            Ok(GraphTarget::Graph(iri))
617        } else {
618            // Try to read a bare IRI as GRAPH <iri>
619            if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'<') {
620                let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
621                *pos = new_pos;
622                Ok(GraphTarget::Graph(iri))
623            } else {
624                let (ln, col) = line_col(input, *pos);
625                Err(
626                    UpdateParseError::new("expected DEFAULT, GRAPH <iri>, or <iri>", *pos)
627                        .with_location(ln, col),
628                )
629            }
630        }
631    }
632
633    // ── quad / triple data blocks ───────────────────────────────────────────
634
635    /// Parse QuadData per SPARQL 1.1 Update grammar:
636    ///   QuadData ::= '{' Quads '}'
637    ///   Quads    ::= TriplesTemplate? ( 'GRAPH' VarOrIri '{' TriplesTemplate? '}' '.'? TriplesTemplate? )*
638    ///
639    /// This handles both `{ triples }` (default graph) and
640    /// `{ GRAPH <iri> { triples } }` (named graph).
641    fn parse_quad_data(
642        &self,
643        input: &str,
644        pos: &mut usize,
645    ) -> Result<(Option<String>, Vec<TriplePattern>), UpdateParseError> {
646        *pos = self.skip_ws(input, *pos);
647
648        // Expect the outer opening brace of QuadData.
649        if *pos >= input.len() || input.as_bytes().get(*pos) != Some(&b'{') {
650            let (ln, col) = line_col(input, *pos);
651            return Err(
652                UpdateParseError::new("expected '{' to open quad data block", *pos)
653                    .with_location(ln, col),
654            );
655        }
656        *pos += 1; // consume outer '{'
657
658        *pos = self.skip_ws(input, *pos);
659
660        // Check whether the content starts with GRAPH keyword (named graph quad).
661        if self.match_keyword(input, *pos, "GRAPH") {
662            *pos = self.consume_keyword(input, *pos, "GRAPH")?;
663            *pos = self.skip_ws(input, *pos);
664            let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
665            *pos = new_pos;
666            *pos = self.skip_ws(input, *pos);
667
668            // Parse the inner brace block `{ triples }`.
669            let triples = self.parse_brace_block(input, pos)?;
670
671            // Consume optional trailing '.' after the GRAPH block.
672            *pos = self.skip_ws(input, *pos);
673            if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'.') {
674                *pos += 1;
675            }
676
677            // Consume outer closing brace.
678            *pos = self.skip_ws(input, *pos);
679            if *pos >= input.len() || input.as_bytes().get(*pos) != Some(&b'}') {
680                let (ln, col) = line_col(input, *pos);
681                return Err(
682                    UpdateParseError::new("expected '}' to close quad data block", *pos)
683                        .with_location(ln, col),
684                );
685            }
686            *pos += 1;
687
688            Ok((Some(iri), triples))
689        } else {
690            // Default graph triples: parse triples until the outer '}'.
691            let mut triples = Vec::new();
692            loop {
693                *pos = self.skip_ws(input, *pos);
694                if *pos >= input.len() {
695                    let (ln, col) = line_col(input, *pos);
696                    return Err(UpdateParseError::new(
697                        "unexpected end of input, expected '}'",
698                        *pos,
699                    )
700                    .with_location(ln, col));
701                }
702                if input.as_bytes().get(*pos) == Some(&b'}') {
703                    *pos += 1;
704                    break;
705                }
706
707                let triple = self.parse_triple_pattern(input, pos)?;
708                triples.push(triple);
709
710                *pos = self.skip_ws(input, *pos);
711                // Consume optional '.'
712                if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'.') {
713                    *pos += 1;
714                }
715            }
716
717            Ok((None, triples))
718        }
719    }
720
721    /// Parse `{ triple1 . triple2 . ... }`.
722    fn parse_brace_block(
723        &self,
724        input: &str,
725        pos: &mut usize,
726    ) -> Result<Vec<TriplePattern>, UpdateParseError> {
727        *pos = self.skip_ws(input, *pos);
728        if *pos >= input.len() || input.as_bytes().get(*pos) != Some(&b'{') {
729            let (ln, col) = line_col(input, *pos);
730            return Err(UpdateParseError::new("expected '{'", *pos).with_location(ln, col));
731        }
732        *pos += 1; // consume '{'
733
734        let mut triples = Vec::new();
735        loop {
736            *pos = self.skip_ws(input, *pos);
737            if *pos >= input.len() {
738                let (ln, col) = line_col(input, *pos);
739                return Err(
740                    UpdateParseError::new("unexpected end of input, expected '}'", *pos)
741                        .with_location(ln, col),
742                );
743            }
744            if input.as_bytes().get(*pos) == Some(&b'}') {
745                *pos += 1;
746                break;
747            }
748
749            let triple = self.parse_triple_pattern(input, pos)?;
750            triples.push(triple);
751
752            *pos = self.skip_ws(input, *pos);
753            // Consume optional '.'
754            if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'.') {
755                *pos += 1;
756            }
757        }
758
759        Ok(triples)
760    }
761
762    /// Parse a single triple pattern: subject predicate object.
763    fn parse_triple_pattern(
764        &self,
765        input: &str,
766        pos: &mut usize,
767    ) -> Result<TriplePattern, UpdateParseError> {
768        *pos = self.skip_ws(input, *pos);
769        let subject = self.read_term(input, pos)?;
770        *pos = self.skip_ws(input, *pos);
771        let predicate = self.read_term(input, pos)?;
772        *pos = self.skip_ws(input, *pos);
773        let object = self.read_term(input, pos)?;
774        Ok(TriplePattern::new(subject, predicate, object))
775    }
776
777    // ── low-level token readers ─────────────────────────────────────────────
778
779    /// Read a single term (IRI, prefixed name, variable, literal, blank node, or 'a').
780    fn read_term(&self, input: &str, pos: &mut usize) -> Result<String, UpdateParseError> {
781        *pos = self.skip_ws(input, *pos);
782        if *pos >= input.len() {
783            let (ln, col) = line_col(input, *pos);
784            return Err(
785                UpdateParseError::new("unexpected end of input while reading term", *pos)
786                    .with_location(ln, col),
787            );
788        }
789
790        let ch = input.as_bytes()[*pos];
791
792        // IRI reference
793        if ch == b'<' {
794            let (iri, new_pos) = self.read_iri_ref(input, *pos)?;
795            *pos = new_pos;
796            return Ok(format!("<{}>", iri));
797        }
798
799        // Variable
800        if ch == b'?' || ch == b'$' {
801            let start = *pos;
802            *pos += 1; // skip ? or $
803            while *pos < input.len() && is_name_char(input.as_bytes()[*pos]) {
804                *pos += 1;
805            }
806            return Ok(input[start..*pos].to_string());
807        }
808
809        // Literal
810        if ch == b'"' || ch == b'\'' {
811            return self.read_literal(input, pos);
812        }
813
814        // Blank node
815        if ch == b'_' && input.as_bytes().get(*pos + 1) == Some(&b':') {
816            let start = *pos;
817            *pos += 2;
818            while *pos < input.len() && is_name_char(input.as_bytes()[*pos]) {
819                *pos += 1;
820            }
821            return Ok(input[start..*pos].to_string());
822        }
823
824        // Keyword 'a' (rdf:type shorthand)
825        if ch == b'a'
826            && (*pos + 1 >= input.len()
827                || !is_name_char(input.as_bytes().get(*pos + 1).copied().unwrap_or(b' ')))
828        {
829            *pos += 1;
830            return Ok("<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>".to_string());
831        }
832
833        // Prefixed name (prefix:local)
834        let start = *pos;
835        while *pos < input.len() && is_name_char(input.as_bytes()[*pos]) {
836            *pos += 1;
837        }
838        if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b':') {
839            let prefix = &input[start..*pos];
840            *pos += 1; // skip ':'
841            let local_start = *pos;
842            while *pos < input.len() && is_pname_local_char(input.as_bytes()[*pos]) {
843                *pos += 1;
844            }
845            let local = &input[local_start..*pos];
846
847            // Expand prefix if known
848            if let Some(ns) = self.prefixes.get(prefix) {
849                return Ok(format!("<{}{}>", ns, local));
850            }
851            return Ok(format!("{}:{}", prefix, local));
852        }
853
854        // Numeric literal
855        *pos = start; // reset
856        if ch.is_ascii_digit() || ch == b'+' || ch == b'-' {
857            return self.read_numeric_literal(input, pos);
858        }
859
860        // Boolean literals
861        if self.match_keyword(input, *pos, "true") {
862            *pos += 4;
863            return Ok("\"true\"^^<http://www.w3.org/2001/XMLSchema#boolean>".to_string());
864        }
865        if self.match_keyword(input, *pos, "false") {
866            *pos += 5;
867            return Ok("\"false\"^^<http://www.w3.org/2001/XMLSchema#boolean>".to_string());
868        }
869
870        let (ln, col) = line_col(input, *pos);
871        let snippet: String = input[*pos..].chars().take(20).collect();
872        Err(
873            UpdateParseError::new(format!("unexpected token: '{}'", snippet), *pos)
874                .with_location(ln, col),
875        )
876    }
877
878    /// Read a string literal (supports double-quoted and single-quoted).
879    fn read_literal(&self, input: &str, pos: &mut usize) -> Result<String, UpdateParseError> {
880        let quote = input.as_bytes()[*pos];
881        let start = *pos;
882        *pos += 1; // skip opening quote
883        let mut value = String::new();
884
885        while *pos < input.len() {
886            let ch = input.as_bytes()[*pos];
887            if ch == b'\\' && *pos + 1 < input.len() {
888                let esc = input.as_bytes()[*pos + 1];
889                let escaped = match esc {
890                    b'n' => '\n',
891                    b't' => '\t',
892                    b'\\' => '\\',
893                    b'"' => '"',
894                    b'\'' => '\'',
895                    _ => {
896                        *pos += 2;
897                        continue;
898                    }
899                };
900                value.push(escaped);
901                *pos += 2;
902            } else if ch == quote {
903                *pos += 1; // skip closing quote
904                           // Check for ^^<datatype> or @lang
905                if *pos < input.len()
906                    && input.as_bytes().get(*pos) == Some(&b'^')
907                    && input.as_bytes().get(*pos + 1) == Some(&b'^')
908                {
909                    *pos += 2;
910                    if input.as_bytes().get(*pos) == Some(&b'<') {
911                        let (dt, new_pos) = self.read_iri_ref(input, *pos)?;
912                        *pos = new_pos;
913                        return Ok(format!("\"{}\"^^<{}>", value, dt));
914                    }
915                }
916                if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'@') {
917                    *pos += 1;
918                    let lang_start = *pos;
919                    while *pos < input.len()
920                        && (input.as_bytes()[*pos].is_ascii_alphanumeric()
921                            || input.as_bytes()[*pos] == b'-')
922                    {
923                        *pos += 1;
924                    }
925                    let lang = &input[lang_start..*pos];
926                    return Ok(format!("\"{}\"@{}", value, lang));
927                }
928                return Ok(format!("\"{}\"", value));
929            } else {
930                value.push(ch as char);
931                *pos += 1;
932            }
933        }
934
935        let (ln, col) = line_col(input, start);
936        Err(UpdateParseError::new("unterminated string literal", start).with_location(ln, col))
937    }
938
939    /// Read a numeric literal (integer or decimal).
940    fn read_numeric_literal(
941        &self,
942        input: &str,
943        pos: &mut usize,
944    ) -> Result<String, UpdateParseError> {
945        let start = *pos;
946        // Optional sign
947        if *pos < input.len() && (input.as_bytes()[*pos] == b'+' || input.as_bytes()[*pos] == b'-')
948        {
949            *pos += 1;
950        }
951        // Digits
952        while *pos < input.len() && input.as_bytes()[*pos].is_ascii_digit() {
953            *pos += 1;
954        }
955        // Optional decimal part
956        let mut is_decimal = false;
957        if *pos < input.len() && input.as_bytes().get(*pos) == Some(&b'.') {
958            is_decimal = true;
959            *pos += 1;
960            while *pos < input.len() && input.as_bytes()[*pos].is_ascii_digit() {
961                *pos += 1;
962            }
963        }
964        // Optional exponent
965        if *pos < input.len() && (input.as_bytes()[*pos] == b'e' || input.as_bytes()[*pos] == b'E')
966        {
967            is_decimal = true;
968            *pos += 1;
969            if *pos < input.len()
970                && (input.as_bytes()[*pos] == b'+' || input.as_bytes()[*pos] == b'-')
971            {
972                *pos += 1;
973            }
974            while *pos < input.len() && input.as_bytes()[*pos].is_ascii_digit() {
975                *pos += 1;
976            }
977        }
978
979        if *pos == start {
980            let (ln, col) = line_col(input, *pos);
981            return Err(
982                UpdateParseError::new("expected numeric literal", *pos).with_location(ln, col)
983            );
984        }
985
986        let text = &input[start..*pos];
987        if is_decimal {
988            Ok(format!(
989                "\"{}\"^^<http://www.w3.org/2001/XMLSchema#double>",
990                text
991            ))
992        } else {
993            Ok(format!(
994                "\"{}\"^^<http://www.w3.org/2001/XMLSchema#integer>",
995                text
996            ))
997        }
998    }
999
1000    /// Read an IRI enclosed in `< >`. Returns the IRI without angle brackets.
1001    fn read_iri_ref(&self, input: &str, pos: usize) -> Result<(String, usize), UpdateParseError> {
1002        if pos >= input.len() || input.as_bytes().get(pos) != Some(&b'<') {
1003            let (ln, col) = line_col(input, pos);
1004            return Err(
1005                UpdateParseError::new("expected '<' to start IRI reference", pos)
1006                    .with_location(ln, col),
1007            );
1008        }
1009        let start = pos + 1;
1010        let mut end = start;
1011        while end < input.len() && input.as_bytes()[end] != b'>' {
1012            end += 1;
1013        }
1014        if end >= input.len() {
1015            let (ln, col) = line_col(input, pos);
1016            return Err(
1017                UpdateParseError::new("unterminated IRI reference", pos).with_location(ln, col)
1018            );
1019        }
1020        let iri = input[start..end].to_string();
1021        Ok((iri, end + 1))
1022    }
1023
1024    /// Read a prefix label (e.g., `ex:` or `:`).
1025    fn read_prefix_label(
1026        &self,
1027        input: &str,
1028        pos: usize,
1029    ) -> Result<(String, usize), UpdateParseError> {
1030        let start = pos;
1031        let mut p = pos;
1032        while p < input.len() && input.as_bytes()[p] != b':' {
1033            p += 1;
1034        }
1035        if p >= input.len() {
1036            let (ln, col) = line_col(input, pos);
1037            return Err(
1038                UpdateParseError::new("expected ':' in prefix declaration", pos)
1039                    .with_location(ln, col),
1040            );
1041        }
1042        let prefix = input[start..p].trim().to_string();
1043        Ok((prefix, p + 1)) // skip ':'
1044    }
1045
1046    // ── utility helpers ─────────────────────────────────────────────────────
1047
1048    fn skip_ws(&self, input: &str, mut pos: usize) -> usize {
1049        let bytes = input.as_bytes();
1050        while pos < bytes.len() {
1051            if bytes[pos].is_ascii_whitespace() {
1052                pos += 1;
1053            } else if bytes[pos] == b'#' {
1054                // Skip line comment
1055                while pos < bytes.len() && bytes[pos] != b'\n' {
1056                    pos += 1;
1057                }
1058            } else {
1059                break;
1060            }
1061        }
1062        pos
1063    }
1064
1065    fn match_keyword(&self, input: &str, pos: usize, kw: &str) -> bool {
1066        let end = pos + kw.len();
1067        if end > input.len() {
1068            return false;
1069        }
1070        if !input[pos..end].eq_ignore_ascii_case(kw) {
1071            return false;
1072        }
1073        // Ensure keyword boundary
1074        end >= input.len() || !is_name_char(input.as_bytes()[end])
1075    }
1076
1077    fn consume_keyword(
1078        &self,
1079        input: &str,
1080        pos: usize,
1081        kw: &str,
1082    ) -> Result<usize, UpdateParseError> {
1083        if !self.match_keyword(input, pos, kw) {
1084            let (ln, col) = line_col(input, pos);
1085            let snippet: String = input[pos..].chars().take(20).collect();
1086            return Err(UpdateParseError::new(
1087                format!("expected keyword '{}', found: '{}'", kw, snippet),
1088                pos,
1089            )
1090            .with_location(ln, col));
1091        }
1092        Ok(pos + kw.len())
1093    }
1094
1095    /// Try to consume an optional keyword. Returns `true` if found.
1096    fn try_consume_keyword(&self, input: &str, pos: &mut usize, kw: &str) -> bool {
1097        if self.match_keyword(input, *pos, kw) {
1098            *pos += kw.len();
1099            true
1100        } else {
1101            false
1102        }
1103    }
1104}
1105
1106// ── character class helpers ─────────────────────────────────────────────────
1107
1108fn is_name_char(b: u8) -> bool {
1109    b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
1110}
1111
1112fn is_pname_local_char(b: u8) -> bool {
1113    b.is_ascii_alphanumeric() || b == b'_' || b == b'-' || b == b'.'
1114}
1115
1116// ────────────────────────────────────────────────────────────────────────────
1117// Convenience functions
1118// ────────────────────────────────────────────────────────────────────────────
1119
1120/// Parse a SPARQL Update request from a string.
1121pub fn parse_update(input: &str) -> Result<UpdateRequest, UpdateParseError> {
1122    let mut parser = UpdateParser::new();
1123    parser.parse(input)
1124}
1125
1126/// Parse a SPARQL Update request with pre-defined prefixes.
1127pub fn parse_update_with_prefixes(
1128    input: &str,
1129    prefixes: HashMap<String, String>,
1130) -> Result<UpdateRequest, UpdateParseError> {
1131    let mut parser = UpdateParser::with_prefixes(prefixes);
1132    parser.parse(input)
1133}