Skip to main content

oxirs_ttl/patch/
mod.rs

1//! RDF Patch Protocol implementation
2//!
3//! RDF Patch is a format for expressing changes to RDF datasets.
4//! Each patch consists of optional header lines followed by change lines.
5//!
6//! # Format Overview
7//!
8//! ```text
9//! H id <uuid>
10//! H prev <uuid>
11//! TX
12//! PA ex <http://example.org/>
13//! A <http://example.org/s> <http://example.org/p> <http://example.org/o>
14//! D <http://example.org/s> <http://example.org/p> <http://example.org/old>
15//! TC
16//! ```
17//!
18//! Line prefixes:
19//! - `H`  — header (version, id, prev)
20//! - `TX` — transaction begin
21//! - `TC` — transaction commit
22//! - `TA` — transaction abort
23//! - `PA` — add prefix
24//! - `PD` — delete prefix
25//! - `A`  — add triple or quad
26//! - `D`  — delete triple or quad
27//!
28//! # References
29//!
30//! <https://afs.github.io/rdf-patch/>
31
32use crate::writer::{RdfTerm, TermType};
33use std::collections::{BTreeMap, HashSet};
34use std::fmt;
35use std::io::{BufRead, BufReader, Read};
36
37// ─── Error ───────────────────────────────────────────────────────────────────
38
39/// Error produced when parsing an RDF Patch document
40#[derive(Debug, Clone)]
41pub struct PatchError {
42    /// 1-based line number where the error occurred
43    pub line: usize,
44    /// Human-readable description
45    pub message: String,
46}
47
48impl PatchError {
49    fn new(line: usize, message: impl Into<String>) -> Self {
50        Self {
51            line,
52            message: message.into(),
53        }
54    }
55
56    fn at(line: usize, msg: impl fmt::Display) -> Self {
57        Self::new(line, msg.to_string())
58    }
59}
60
61impl fmt::Display for PatchError {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(f, "patch error at line {}: {}", self.line, self.message)
64    }
65}
66
67impl std::error::Error for PatchError {}
68
69/// Convenience result alias
70pub type PatchResult<T> = Result<T, PatchError>;
71
72// ─── Data model ──────────────────────────────────────────────────────────────
73
74/// A header entry in an RDF Patch document
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum PatchHeader {
77    /// `H version <value>` — patch format version
78    Version(String),
79    /// `H prev <uuid>` — IRI/UUID of the previous patch in the chain
80    Previous(String),
81    /// `H id <uuid>` — IRI/UUID identifying this patch
82    Id(String),
83    /// Any other `H key <value>` header not defined in the spec
84    Unknown {
85        /// The header key
86        key: String,
87        /// The header value
88        value: String,
89    },
90}
91
92impl PatchHeader {
93    /// Return the header key string as it appears in the serialised format
94    pub fn key(&self) -> &str {
95        match self {
96            PatchHeader::Version(_) => "version",
97            PatchHeader::Previous(_) => "prev",
98            PatchHeader::Id(_) => "id",
99            PatchHeader::Unknown { key, .. } => key.as_str(),
100        }
101    }
102
103    /// Return the header value string
104    pub fn value(&self) -> &str {
105        match self {
106            PatchHeader::Version(v) | PatchHeader::Previous(v) | PatchHeader::Id(v) => v.as_str(),
107            PatchHeader::Unknown { value, .. } => value.as_str(),
108        }
109    }
110}
111
112impl fmt::Display for PatchHeader {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "H {} {}", self.key(), self.value())
115    }
116}
117
118/// A subject/object position that accepts either a named node, blank node, or literal.
119/// Used internally for parsed term positions.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct PatchTerm(pub RdfTerm);
122
123impl PatchTerm {
124    /// Create from an IRI (angle-bracket or prefixed form, already resolved)
125    pub fn iri(iri: impl Into<String>) -> Self {
126        Self(RdfTerm::iri(iri))
127    }
128
129    /// Create from a blank node identifier
130    pub fn blank_node(id: impl Into<String>) -> Self {
131        Self(RdfTerm::blank_node(id))
132    }
133
134    /// Create from a plain literal value
135    pub fn literal(value: impl Into<String>) -> Self {
136        Self(RdfTerm::simple_literal(value))
137    }
138
139    /// Create from a language-tagged literal
140    pub fn lang_literal(value: impl Into<String>, lang: impl Into<String>) -> Self {
141        Self(RdfTerm::lang_literal(value, lang))
142    }
143
144    /// Create from a typed literal
145    pub fn typed_literal(value: impl Into<String>, datatype: impl Into<String>) -> Self {
146        Self(RdfTerm::typed_literal(value, datatype))
147    }
148
149    /// Access the underlying [`RdfTerm`]
150    pub fn term(&self) -> &RdfTerm {
151        &self.0
152    }
153
154    /// Return `true` if this term is an IRI
155    pub fn is_iri(&self) -> bool {
156        self.0.term_type == TermType::Iri
157    }
158
159    /// Return `true` if this term is a blank node
160    pub fn is_blank_node(&self) -> bool {
161        self.0.term_type == TermType::BlankNode
162    }
163
164    /// Return the lexical value
165    pub fn value(&self) -> &str {
166        &self.0.value
167    }
168}
169
170impl fmt::Display for PatchTerm {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        match &self.0.term_type {
173            TermType::Iri => write!(f, "<{}>", self.0.value),
174            TermType::BlankNode => write!(f, "_:{}", self.0.value),
175            TermType::Literal { datatype, lang } => {
176                // Escape internal quotes
177                let escaped = self.0.value.replace('\\', "\\\\").replace('"', "\\\"");
178                write!(f, "\"{escaped}\"")?;
179                if let Some(l) = lang {
180                    write!(f, "@{l}")?;
181                } else if let Some(dt) = datatype {
182                    write!(f, "^^<{dt}>")?;
183                }
184                Ok(())
185            }
186        }
187    }
188}
189
190/// A triple (subject, predicate, object) using [`PatchTerm`]
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct PatchTriple {
193    /// The subject term
194    pub subject: PatchTerm,
195    /// The predicate term
196    pub predicate: PatchTerm,
197    /// The object term
198    pub object: PatchTerm,
199}
200
201impl PatchTriple {
202    /// Construct a new triple from three terms
203    pub fn new(subject: PatchTerm, predicate: PatchTerm, object: PatchTerm) -> Self {
204        Self {
205            subject,
206            predicate,
207            object,
208        }
209    }
210}
211
212impl fmt::Display for PatchTriple {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        write!(f, "{} {} {} .", self.subject, self.predicate, self.object)
215    }
216}
217
218/// A quad (subject, predicate, object, graph) using [`PatchTerm`]
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct PatchQuad {
221    /// The subject term
222    pub subject: PatchTerm,
223    /// The predicate term
224    pub predicate: PatchTerm,
225    /// The object term
226    pub object: PatchTerm,
227    /// The named graph term
228    pub graph: PatchTerm,
229}
230
231impl PatchQuad {
232    /// Construct a new quad
233    pub fn new(
234        subject: PatchTerm,
235        predicate: PatchTerm,
236        object: PatchTerm,
237        graph: PatchTerm,
238    ) -> Self {
239        Self {
240            subject,
241            predicate,
242            object,
243            graph,
244        }
245    }
246}
247
248impl fmt::Display for PatchQuad {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        write!(
251            f,
252            "{} {} {} {} .",
253            self.subject, self.predicate, self.object, self.graph
254        )
255    }
256}
257
258/// A single change line in an RDF Patch document
259#[derive(Debug, Clone, PartialEq, Eq)]
260pub enum PatchChange {
261    /// `PA prefix <iri>` — add a prefix declaration
262    AddPrefix {
263        /// Namespace prefix label
264        prefix: String,
265        /// IRI bound to the prefix
266        iri: String,
267    },
268    /// `PD prefix <iri>` — delete a prefix declaration
269    DeletePrefix {
270        /// Namespace prefix label
271        prefix: String,
272        /// IRI bound to the prefix
273        iri: String,
274    },
275    /// `A <s> <p> <o> .` — add a triple
276    AddTriple(PatchTriple),
277    /// `D <s> <p> <o> .` — delete a triple
278    DeleteTriple(PatchTriple),
279    /// `A <s> <p> <o> <g> .` — add a quad
280    AddQuad(PatchQuad),
281    /// `D <s> <p> <o> <g> .` — delete a quad
282    DeleteQuad(PatchQuad),
283    /// `TX` — begin a transaction block
284    TransactionBegin,
285    /// `TC` — commit a transaction block
286    TransactionCommit,
287    /// `TA` — abort a transaction block
288    TransactionAbort,
289}
290
291impl PatchChange {
292    /// Return the line prefix that represents this change in the patch format
293    pub fn line_prefix(&self) -> &'static str {
294        match self {
295            PatchChange::AddPrefix { .. } => "PA",
296            PatchChange::DeletePrefix { .. } => "PD",
297            PatchChange::AddTriple(_) => "A",
298            PatchChange::DeleteTriple(_) => "D",
299            PatchChange::AddQuad(_) => "A",
300            PatchChange::DeleteQuad(_) => "D",
301            PatchChange::TransactionBegin => "TX",
302            PatchChange::TransactionCommit => "TC",
303            PatchChange::TransactionAbort => "TA",
304        }
305    }
306
307    /// Return `true` if this change adds data
308    pub fn is_add(&self) -> bool {
309        matches!(
310            self,
311            PatchChange::AddTriple(_) | PatchChange::AddQuad(_) | PatchChange::AddPrefix { .. }
312        )
313    }
314
315    /// Return `true` if this change deletes data
316    pub fn is_delete(&self) -> bool {
317        matches!(
318            self,
319            PatchChange::DeleteTriple(_)
320                | PatchChange::DeleteQuad(_)
321                | PatchChange::DeletePrefix { .. }
322        )
323    }
324
325    /// Return `true` if this is a transaction control statement
326    pub fn is_transaction_control(&self) -> bool {
327        matches!(
328            self,
329            PatchChange::TransactionBegin
330                | PatchChange::TransactionCommit
331                | PatchChange::TransactionAbort
332        )
333    }
334}
335
336impl fmt::Display for PatchChange {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        match self {
339            PatchChange::AddPrefix { prefix, iri } => {
340                write!(f, "PA {prefix} <{iri}>")
341            }
342            PatchChange::DeletePrefix { prefix, iri } => {
343                write!(f, "PD {prefix} <{iri}>")
344            }
345            PatchChange::AddTriple(t) => write!(f, "A {t}"),
346            PatchChange::DeleteTriple(t) => write!(f, "D {t}"),
347            PatchChange::AddQuad(q) => write!(f, "A {q}"),
348            PatchChange::DeleteQuad(q) => write!(f, "D {q}"),
349            PatchChange::TransactionBegin => write!(f, "TX"),
350            PatchChange::TransactionCommit => write!(f, "TC"),
351            PatchChange::TransactionAbort => write!(f, "TA"),
352        }
353    }
354}
355
356// ─── RdfPatch ────────────────────────────────────────────────────────────────
357
358/// A complete RDF Patch document: a list of headers followed by change lines
359#[derive(Debug, Clone, Default)]
360pub struct RdfPatch {
361    /// Header entries (`H key value`)
362    pub headers: Vec<PatchHeader>,
363    /// Change entries (`TX / TC / TA / PA / PD / A / D`)
364    pub changes: Vec<PatchChange>,
365}
366
367impl RdfPatch {
368    /// Construct an empty patch
369    pub fn new() -> Self {
370        Self::default()
371    }
372
373    /// Construct a patch with headers and changes
374    pub fn with_changes(headers: Vec<PatchHeader>, changes: Vec<PatchChange>) -> Self {
375        Self { headers, changes }
376    }
377
378    /// Return the `id` header value if present
379    pub fn id(&self) -> Option<&str> {
380        self.headers.iter().find_map(|h| {
381            if let PatchHeader::Id(v) = h {
382                Some(v.as_str())
383            } else {
384                None
385            }
386        })
387    }
388
389    /// Return the `prev` header value if present
390    pub fn previous(&self) -> Option<&str> {
391        self.headers.iter().find_map(|h| {
392            if let PatchHeader::Previous(v) = h {
393                Some(v.as_str())
394            } else {
395                None
396            }
397        })
398    }
399
400    /// Count how many triple/quad additions are in the patch
401    pub fn add_count(&self) -> usize {
402        self.changes
403            .iter()
404            .filter(|c| matches!(c, PatchChange::AddTriple(_) | PatchChange::AddQuad(_)))
405            .count()
406    }
407
408    /// Count how many triple/quad deletions are in the patch
409    pub fn delete_count(&self) -> usize {
410        self.changes
411            .iter()
412            .filter(|c| matches!(c, PatchChange::DeleteTriple(_) | PatchChange::DeleteQuad(_)))
413            .count()
414    }
415
416    /// Return `true` if the patch contains no headers and no changes
417    pub fn is_empty(&self) -> bool {
418        self.headers.is_empty() && self.changes.is_empty()
419    }
420}
421
422// ─── Statistics ──────────────────────────────────────────────────────────────
423
424/// Statistics collected when applying a patch to a graph
425#[derive(Debug, Clone, Default, PartialEq, Eq)]
426pub struct PatchStats {
427    /// Number of triples that were actually inserted
428    pub triples_added: usize,
429    /// Number of triples that were actually removed
430    pub triples_deleted: usize,
431    /// Number of prefix declarations that were added
432    pub prefixes_added: usize,
433    /// Number of prefix declarations that were removed
434    pub prefixes_deleted: usize,
435    /// Number of transaction blocks encountered
436    pub transactions: usize,
437    /// Number of transaction aborts encountered
438    pub aborts: usize,
439}
440
441// ─── In-memory graph ─────────────────────────────────────────────────────────
442
443/// A minimal in-memory RDF graph used for patch application and diff generation.
444///
445/// Triples are stored as `(subject, predicate, object)` tuples of [`PatchTerm`].
446/// Prefix mappings are stored separately.
447#[derive(Debug, Clone, Default)]
448pub struct Graph {
449    /// Set of (subject, predicate, object) triples
450    pub triples: HashSet<String>,
451    /// Prefix → IRI mappings
452    pub prefixes: BTreeMap<String, String>,
453    /// Raw triple objects for retrieval (mirrors `triples`)
454    triple_objects: Vec<PatchTriple>,
455}
456
457impl Graph {
458    /// Construct an empty graph
459    pub fn new() -> Self {
460        Self::default()
461    }
462
463    /// Add a triple to the graph; returns `true` if newly inserted
464    pub fn add_triple(&mut self, triple: PatchTriple) -> bool {
465        let key = Self::triple_key(&triple);
466        if self.triples.insert(key) {
467            self.triple_objects.push(triple);
468            true
469        } else {
470            false
471        }
472    }
473
474    /// Remove a triple from the graph; returns `true` if it was present
475    pub fn remove_triple(&mut self, triple: &PatchTriple) -> bool {
476        let key = Self::triple_key(triple);
477        if self.triples.remove(&key) {
478            self.triple_objects.retain(|t| Self::triple_key(t) != key);
479            true
480        } else {
481            false
482        }
483    }
484
485    /// Return `true` if the triple is present in the graph
486    pub fn contains(&self, triple: &PatchTriple) -> bool {
487        self.triples.contains(&Self::triple_key(triple))
488    }
489
490    /// Number of triples in the graph
491    pub fn len(&self) -> usize {
492        self.triples.len()
493    }
494
495    /// Return `true` if the graph has no triples
496    pub fn is_empty(&self) -> bool {
497        self.triples.is_empty()
498    }
499
500    /// Iterate over all triples in the graph
501    pub fn iter(&self) -> impl Iterator<Item = &PatchTriple> {
502        self.triple_objects.iter()
503    }
504
505    fn triple_key(t: &PatchTriple) -> String {
506        format!("{}\x00{}\x00{}", t.subject, t.predicate, t.object)
507    }
508}
509
510// ─── PatchParser ─────────────────────────────────────────────────────────────
511
512/// Parser for the RDF Patch text format
513pub struct PatchParser;
514
515impl PatchParser {
516    /// Parse an entire RDF Patch document from a string
517    pub fn parse(input: &str) -> PatchResult<RdfPatch> {
518        let mut headers = Vec::new();
519        let mut changes = Vec::new();
520        let mut prefixes: BTreeMap<String, String> = BTreeMap::new();
521
522        for (idx, raw_line) in input.lines().enumerate() {
523            let line_no = idx + 1;
524            let line = raw_line.trim();
525
526            // Skip blank lines and comments
527            if line.is_empty() || line.starts_with('#') {
528                continue;
529            }
530
531            if let Some(rest) = line.strip_prefix("H ") {
532                let header = Self::parse_header(rest.trim(), line_no)?;
533                headers.push(header);
534            } else if line == "TX" {
535                changes.push(PatchChange::TransactionBegin);
536            } else if line == "TC" {
537                changes.push(PatchChange::TransactionCommit);
538            } else if line == "TA" {
539                changes.push(PatchChange::TransactionAbort);
540            } else if let Some(rest) = line.strip_prefix("PA ") {
541                let (prefix, iri) = Self::parse_prefix_decl(rest.trim(), line_no)?;
542                prefixes.insert(prefix.clone(), iri.clone());
543                changes.push(PatchChange::AddPrefix { prefix, iri });
544            } else if let Some(rest) = line.strip_prefix("PD ") {
545                let (prefix, iri) = Self::parse_prefix_decl(rest.trim(), line_no)?;
546                changes.push(PatchChange::DeletePrefix { prefix, iri });
547            } else if let Some(rest) = line.strip_prefix("A ") {
548                let change = Self::parse_triple_or_quad("A", rest.trim(), &prefixes, line_no)?;
549                changes.push(change);
550            } else if let Some(rest) = line.strip_prefix("D ") {
551                let change = Self::parse_triple_or_quad("D", rest.trim(), &prefixes, line_no)?;
552                changes.push(change);
553            } else {
554                return Err(PatchError::at(
555                    line_no,
556                    format!("unrecognised line: {line:?}"),
557                ));
558            }
559        }
560
561        Ok(RdfPatch { headers, changes })
562    }
563
564    /// Create a streaming iterator that parses one [`PatchChange`] at a time.
565    /// Headers are skipped in streaming mode (only change lines are yielded).
566    pub fn parse_streaming(reader: impl Read) -> impl Iterator<Item = PatchResult<PatchChange>> {
567        StreamingPatchParser::new(reader)
568    }
569
570    // ── Internal helpers ──────────────────────────────────────────────────
571
572    fn parse_header(rest: &str, line_no: usize) -> PatchResult<PatchHeader> {
573        // rest is `key <value>` or `key value`
574        let mut parts = rest.splitn(2, ' ');
575        let key = parts
576            .next()
577            .ok_or_else(|| PatchError::at(line_no, "missing header key"))?
578            .trim();
579        let value_raw = parts.next().unwrap_or("").trim();
580        let value = strip_angle_brackets(value_raw);
581        match key {
582            "version" => Ok(PatchHeader::Version(value.to_string())),
583            "prev" => Ok(PatchHeader::Previous(value.to_string())),
584            "id" => Ok(PatchHeader::Id(value.to_string())),
585            other => Ok(PatchHeader::Unknown {
586                key: other.to_string(),
587                value: value.to_string(),
588            }),
589        }
590    }
591
592    fn parse_prefix_decl(rest: &str, line_no: usize) -> PatchResult<(String, String)> {
593        // rest is `prefix <iri>` or `prefix: <iri>`
594        let mut parts = rest.splitn(2, ' ');
595        let prefix_raw = parts
596            .next()
597            .ok_or_else(|| PatchError::at(line_no, "missing prefix name"))?
598            .trim_end_matches(':');
599        let iri_raw = parts
600            .next()
601            .ok_or_else(|| PatchError::at(line_no, "missing prefix IRI"))?
602            .trim();
603        let iri = strip_angle_brackets(iri_raw);
604        Ok((prefix_raw.to_string(), iri.to_string()))
605    }
606
607    fn parse_triple_or_quad(
608        op: &str,
609        rest: &str,
610        prefixes: &BTreeMap<String, String>,
611        line_no: usize,
612    ) -> PatchResult<PatchChange> {
613        // Strip trailing ' .' if present
614        let rest = rest.trim_end_matches('.').trim();
615        let terms = tokenise_terms(rest, prefixes, line_no)?;
616        match terms.len() {
617            3 => {
618                let triple = PatchTriple::new(terms[0].clone(), terms[1].clone(), terms[2].clone());
619                if op == "A" {
620                    Ok(PatchChange::AddTriple(triple))
621                } else {
622                    Ok(PatchChange::DeleteTriple(triple))
623                }
624            }
625            4 => {
626                let quad = PatchQuad::new(
627                    terms[0].clone(),
628                    terms[1].clone(),
629                    terms[2].clone(),
630                    terms[3].clone(),
631                );
632                if op == "A" {
633                    Ok(PatchChange::AddQuad(quad))
634                } else {
635                    Ok(PatchChange::DeleteQuad(quad))
636                }
637            }
638            n => Err(PatchError::at(
639                line_no,
640                format!("expected 3 or 4 terms, got {n}"),
641            )),
642        }
643    }
644}
645
646// ─── Streaming parser ────────────────────────────────────────────────────────
647
648struct StreamingPatchParser<R: Read> {
649    reader: BufReader<R>,
650    line_no: usize,
651    prefixes: BTreeMap<String, String>,
652    done: bool,
653}
654
655impl<R: Read> StreamingPatchParser<R> {
656    fn new(reader: R) -> Self {
657        Self {
658            reader: BufReader::new(reader),
659            line_no: 0,
660            prefixes: BTreeMap::new(),
661            done: false,
662        }
663    }
664}
665
666impl<R: Read> Iterator for StreamingPatchParser<R> {
667    type Item = PatchResult<PatchChange>;
668
669    fn next(&mut self) -> Option<Self::Item> {
670        if self.done {
671            return None;
672        }
673        loop {
674            let mut raw = String::new();
675            match self.reader.read_line(&mut raw) {
676                Ok(0) => {
677                    self.done = true;
678                    return None;
679                }
680                Err(e) => {
681                    self.done = true;
682                    return Some(Err(PatchError::at(self.line_no, e.to_string())));
683                }
684                Ok(_) => {}
685            }
686            self.line_no += 1;
687            let line = raw.trim();
688
689            if line.is_empty() || line.starts_with('#') {
690                continue;
691            }
692
693            // Headers — skip silently in streaming mode
694            if line.starts_with("H ") {
695                continue;
696            }
697
698            let result = if line == "TX" {
699                Ok(PatchChange::TransactionBegin)
700            } else if line == "TC" {
701                Ok(PatchChange::TransactionCommit)
702            } else if line == "TA" {
703                Ok(PatchChange::TransactionAbort)
704            } else if let Some(rest) = line.strip_prefix("PA ") {
705                match parse_prefix_decl_inline(rest.trim(), self.line_no) {
706                    Ok((prefix, iri)) => {
707                        self.prefixes.insert(prefix.clone(), iri.clone());
708                        Ok(PatchChange::AddPrefix { prefix, iri })
709                    }
710                    Err(e) => Err(e),
711                }
712            } else if let Some(rest) = line.strip_prefix("PD ") {
713                match parse_prefix_decl_inline(rest.trim(), self.line_no) {
714                    Ok((prefix, iri)) => Ok(PatchChange::DeletePrefix { prefix, iri }),
715                    Err(e) => Err(e),
716                }
717            } else if let Some(rest) = line.strip_prefix("A ") {
718                PatchParser::parse_triple_or_quad("A", rest.trim(), &self.prefixes, self.line_no)
719            } else if let Some(rest) = line.strip_prefix("D ") {
720                PatchParser::parse_triple_or_quad("D", rest.trim(), &self.prefixes, self.line_no)
721            } else {
722                Err(PatchError::at(
723                    self.line_no,
724                    format!("unrecognised line: {line:?}"),
725                ))
726            };
727
728            return Some(result);
729        }
730    }
731}
732
733// ─── PatchSerializer ─────────────────────────────────────────────────────────
734
735/// Serializes [`RdfPatch`] documents to the RDF Patch text format
736pub struct PatchSerializer;
737
738impl PatchSerializer {
739    /// Serialize an [`RdfPatch`] to a string
740    pub fn serialize(patch: &RdfPatch) -> String {
741        let mut out = String::new();
742        for header in &patch.headers {
743            out.push_str(&header.to_string());
744            out.push('\n');
745        }
746        if !patch.headers.is_empty() && !patch.changes.is_empty() {
747            out.push('\n');
748        }
749        for change in &patch.changes {
750            out.push_str(&change.to_string());
751            out.push('\n');
752        }
753        out
754    }
755
756    /// Serialize a single [`PatchChange`] line (no newline appended)
757    pub fn serialize_change(change: &PatchChange) -> String {
758        change.to_string()
759    }
760}
761
762// ─── apply_patch ─────────────────────────────────────────────────────────────
763
764/// Apply an [`RdfPatch`] to an in-memory [`Graph`], updating it in place.
765///
766/// Transactions are honoured: changes between `TX`/`TA` are rolled back on abort.
767/// Returns [`PatchStats`] summarising what was modified.
768pub fn apply_patch(graph: &mut Graph, patch: &RdfPatch) -> PatchResult<PatchStats> {
769    let mut stats = PatchStats::default();
770    let mut in_tx = false;
771    // Staged changes for the current transaction block
772    let mut tx_adds: Vec<PatchTriple> = Vec::new();
773    let mut tx_deletes: Vec<PatchTriple> = Vec::new();
774    let mut tx_prefix_adds: Vec<(String, String)> = Vec::new();
775
776    for change in &patch.changes {
777        match change {
778            PatchChange::TransactionBegin => {
779                in_tx = true;
780                tx_adds.clear();
781                tx_deletes.clear();
782                tx_prefix_adds.clear();
783                stats.transactions += 1;
784            }
785            PatchChange::TransactionCommit => {
786                // Commit staged changes
787                for t in tx_adds.drain(..) {
788                    if graph.add_triple(t) {
789                        stats.triples_added += 1;
790                    }
791                }
792                for t in &tx_deletes {
793                    if graph.remove_triple(t) {
794                        stats.triples_deleted += 1;
795                    }
796                }
797                tx_deletes.clear();
798                for (p, i) in tx_prefix_adds.drain(..) {
799                    graph.prefixes.insert(p, i);
800                    stats.prefixes_added += 1;
801                }
802                in_tx = false;
803            }
804            PatchChange::TransactionAbort => {
805                // Discard staged changes
806                tx_adds.clear();
807                tx_deletes.clear();
808                tx_prefix_adds.clear();
809                in_tx = false;
810                stats.aborts += 1;
811            }
812            PatchChange::AddPrefix { prefix, iri } => {
813                if in_tx {
814                    tx_prefix_adds.push((prefix.clone(), iri.clone()));
815                } else {
816                    graph.prefixes.insert(prefix.clone(), iri.clone());
817                    stats.prefixes_added += 1;
818                }
819            }
820            PatchChange::DeletePrefix { prefix, .. } => {
821                graph.prefixes.remove(prefix.as_str());
822                stats.prefixes_deleted += 1;
823            }
824            PatchChange::AddTriple(t) => {
825                if in_tx {
826                    tx_adds.push(t.clone());
827                } else if graph.add_triple(t.clone()) {
828                    stats.triples_added += 1;
829                }
830            }
831            PatchChange::DeleteTriple(t) => {
832                if in_tx {
833                    tx_deletes.push(t.clone());
834                } else if graph.remove_triple(t) {
835                    stats.triples_deleted += 1;
836                }
837            }
838            // Quads are not supported on simple Graph; treat as triple
839            PatchChange::AddQuad(q) => {
840                let t = PatchTriple::new(q.subject.clone(), q.predicate.clone(), q.object.clone());
841                if in_tx {
842                    tx_adds.push(t);
843                } else if graph.add_triple(t) {
844                    stats.triples_added += 1;
845                }
846            }
847            PatchChange::DeleteQuad(q) => {
848                let t = PatchTriple::new(q.subject.clone(), q.predicate.clone(), q.object.clone());
849                if in_tx {
850                    tx_deletes.push(t.clone());
851                } else if graph.remove_triple(&t) {
852                    stats.triples_deleted += 1;
853                }
854            }
855        }
856    }
857
858    Ok(stats)
859}
860
861// ─── diff_to_patch ───────────────────────────────────────────────────────────
862
863/// Generate a minimal [`RdfPatch`] that transforms `old` into `new`.
864///
865/// All deletions come before additions in the generated patch, matching
866/// the convention used by most RDF Patch tools.
867pub fn diff_to_patch(old: &Graph, new: &Graph) -> RdfPatch {
868    let mut changes = Vec::new();
869
870    // Deletes: triples in old but not new
871    for triple in old.iter() {
872        if !new.contains(triple) {
873            changes.push(PatchChange::DeleteTriple(triple.clone()));
874        }
875    }
876
877    // Adds: triples in new but not old
878    for triple in new.iter() {
879        if !old.contains(triple) {
880            changes.push(PatchChange::AddTriple(triple.clone()));
881        }
882    }
883
884    // Prefix adds: in new but not old
885    for (prefix, iri) in &new.prefixes {
886        if old.prefixes.get(prefix) != Some(iri) {
887            changes.push(PatchChange::AddPrefix {
888                prefix: prefix.clone(),
889                iri: iri.clone(),
890            });
891        }
892    }
893
894    // Prefix deletes: in old but not new
895    for (prefix, iri) in &old.prefixes {
896        if !new.prefixes.contains_key(prefix.as_str()) {
897            changes.push(PatchChange::DeletePrefix {
898                prefix: prefix.clone(),
899                iri: iri.clone(),
900            });
901        }
902    }
903
904    RdfPatch {
905        headers: Vec::new(),
906        changes,
907    }
908}
909
910// ─── Term tokeniser ──────────────────────────────────────────────────────────
911
912/// Tokenise a whitespace-separated sequence of RDF terms.
913/// Handles IRIs (`<...>`), blank nodes (`_:id`), literals (`"..."`), and
914/// prefixed names (`prefix:local`).
915fn tokenise_terms(
916    input: &str,
917    prefixes: &BTreeMap<String, String>,
918    line_no: usize,
919) -> PatchResult<Vec<PatchTerm>> {
920    let mut terms = Vec::new();
921    let chars: Vec<char> = input.chars().collect();
922    let mut pos = 0;
923
924    while pos < chars.len() {
925        // Skip whitespace
926        while pos < chars.len() && chars[pos].is_whitespace() {
927            pos += 1;
928        }
929        if pos >= chars.len() {
930            break;
931        }
932
933        if chars[pos] == '<' {
934            // IRI
935            pos += 1;
936            let start = pos;
937            while pos < chars.len() && chars[pos] != '>' {
938                pos += 1;
939            }
940            if pos >= chars.len() {
941                return Err(PatchError::at(line_no, "unterminated IRI"));
942            }
943            let iri: String = chars[start..pos].iter().collect();
944            pos += 1; // consume '>'
945            terms.push(PatchTerm::iri(iri));
946        } else if chars[pos] == '"' {
947            // Literal
948            pos += 1;
949            let mut value = String::new();
950            while pos < chars.len() {
951                if chars[pos] == '\\' && pos + 1 < chars.len() {
952                    pos += 1;
953                    match chars[pos] {
954                        '"' => value.push('"'),
955                        '\\' => value.push('\\'),
956                        'n' => value.push('\n'),
957                        'r' => value.push('\r'),
958                        't' => value.push('\t'),
959                        c => {
960                            value.push('\\');
961                            value.push(c);
962                        }
963                    }
964                    pos += 1;
965                } else if chars[pos] == '"' {
966                    break;
967                } else {
968                    value.push(chars[pos]);
969                    pos += 1;
970                }
971            }
972            if pos >= chars.len() {
973                return Err(PatchError::at(line_no, "unterminated literal"));
974            }
975            pos += 1; // consume closing '"'
976
977            // Check for language tag or datatype
978            if pos < chars.len() && chars[pos] == '@' {
979                pos += 1;
980                let start = pos;
981                while pos < chars.len() && !chars[pos].is_whitespace() {
982                    pos += 1;
983                }
984                let lang: String = chars[start..pos].iter().collect();
985                terms.push(PatchTerm::lang_literal(value, lang));
986            } else if pos + 1 < chars.len() && chars[pos] == '^' && chars[pos + 1] == '^' {
987                pos += 2;
988                if pos >= chars.len() || chars[pos] != '<' {
989                    return Err(PatchError::at(line_no, "expected '<' after '^^'"));
990                }
991                pos += 1;
992                let start = pos;
993                while pos < chars.len() && chars[pos] != '>' {
994                    pos += 1;
995                }
996                if pos >= chars.len() {
997                    return Err(PatchError::at(line_no, "unterminated datatype IRI"));
998                }
999                let dt: String = chars[start..pos].iter().collect();
1000                pos += 1;
1001                terms.push(PatchTerm::typed_literal(value, dt));
1002            } else {
1003                terms.push(PatchTerm::literal(value));
1004            }
1005        } else if pos + 1 < chars.len() && chars[pos] == '_' && chars[pos + 1] == ':' {
1006            // Blank node
1007            pos += 2;
1008            let start = pos;
1009            while pos < chars.len() && !chars[pos].is_whitespace() && chars[pos] != '.' {
1010                pos += 1;
1011            }
1012            let id: String = chars[start..pos].iter().collect();
1013            terms.push(PatchTerm::blank_node(id));
1014        } else if chars[pos] == '.' {
1015            // Trailing dot — stop
1016            pos += 1;
1017        } else {
1018            // Possibly a prefixed name `prefix:local`
1019            let start = pos;
1020            while pos < chars.len() && !chars[pos].is_whitespace() && chars[pos] != '.' {
1021                pos += 1;
1022            }
1023            let token: String = chars[start..pos].iter().collect();
1024            if let Some(colon_pos) = token.find(':') {
1025                let ns = &token[..colon_pos];
1026                let local = &token[colon_pos + 1..];
1027                match prefixes.get(ns) {
1028                    Some(base) => {
1029                        let full = format!("{base}{local}");
1030                        terms.push(PatchTerm::iri(full));
1031                    }
1032                    None => {
1033                        return Err(PatchError::at(
1034                            line_no,
1035                            format!("unknown prefix '{ns}' in '{token}'"),
1036                        ))
1037                    }
1038                }
1039            } else if token.is_empty() || token == "." {
1040                // skip
1041            } else {
1042                return Err(PatchError::at(
1043                    line_no,
1044                    format!("unexpected token '{token}'"),
1045                ));
1046            }
1047        }
1048    }
1049
1050    Ok(terms)
1051}
1052
1053/// Strip surrounding `<...>` from an IRI token, if present
1054fn strip_angle_brackets(s: &str) -> &str {
1055    if s.starts_with('<') && s.ends_with('>') {
1056        &s[1..s.len() - 1]
1057    } else {
1058        s
1059    }
1060}
1061
1062/// Inline prefix-decl parser used in the streaming parser
1063fn parse_prefix_decl_inline(rest: &str, line_no: usize) -> PatchResult<(String, String)> {
1064    let mut parts = rest.splitn(2, ' ');
1065    let prefix_raw = parts
1066        .next()
1067        .ok_or_else(|| PatchError::at(line_no, "missing prefix name"))?
1068        .trim_end_matches(':');
1069    let iri_raw = parts
1070        .next()
1071        .ok_or_else(|| PatchError::at(line_no, "missing prefix IRI"))?
1072        .trim();
1073    let iri = strip_angle_brackets(iri_raw);
1074    Ok((prefix_raw.to_string(), iri.to_string()))
1075}
1076
1077// ─── Tests ───────────────────────────────────────────────────────────────────
1078
1079#[cfg(test)]
1080mod tests {
1081    use super::*;
1082
1083    // Helper to build a simple triple
1084    fn triple(s: &str, p: &str, o: &str) -> PatchTriple {
1085        PatchTriple::new(PatchTerm::iri(s), PatchTerm::iri(p), PatchTerm::iri(o))
1086    }
1087
1088    fn triple_lit(s: &str, p: &str, o: &str) -> PatchTriple {
1089        PatchTriple::new(PatchTerm::iri(s), PatchTerm::iri(p), PatchTerm::literal(o))
1090    }
1091
1092    // ── Header parsing ────────────────────────────────────────────────────
1093
1094    #[test]
1095    fn test_parse_header_id() {
1096        let patch = PatchParser::parse("H id <urn:uuid:1234>\n").expect("should succeed");
1097        assert_eq!(patch.headers.len(), 1);
1098        assert_eq!(patch.id(), Some("urn:uuid:1234"));
1099    }
1100
1101    #[test]
1102    fn test_parse_header_prev() {
1103        let patch = PatchParser::parse("H prev <urn:uuid:abcd>\n").expect("should succeed");
1104        assert_eq!(patch.previous(), Some("urn:uuid:abcd"));
1105    }
1106
1107    #[test]
1108    fn test_parse_header_version() {
1109        let patch = PatchParser::parse("H version 1\n").expect("should succeed");
1110        matches!(&patch.headers[0], PatchHeader::Version(v) if v == "1");
1111    }
1112
1113    #[test]
1114    fn test_parse_header_unknown() {
1115        let patch = PatchParser::parse("H custom myval\n").expect("should succeed");
1116        assert!(matches!(&patch.headers[0], PatchHeader::Unknown { key, .. } if key == "custom"));
1117    }
1118
1119    #[test]
1120    fn test_parse_multiple_headers() {
1121        let input = "H id <urn:1>\nH prev <urn:0>\nH version 2\n";
1122        let patch = PatchParser::parse(input).expect("should succeed");
1123        assert_eq!(patch.headers.len(), 3);
1124    }
1125
1126    // ── Transaction control ───────────────────────────────────────────────
1127
1128    #[test]
1129    fn test_parse_tx_tc() {
1130        let patch = PatchParser::parse("TX\nTC\n").expect("should succeed");
1131        assert_eq!(patch.changes.len(), 2);
1132        assert!(matches!(patch.changes[0], PatchChange::TransactionBegin));
1133        assert!(matches!(patch.changes[1], PatchChange::TransactionCommit));
1134    }
1135
1136    #[test]
1137    fn test_parse_ta() {
1138        let patch = PatchParser::parse("TX\nTA\n").expect("should succeed");
1139        assert!(matches!(patch.changes[1], PatchChange::TransactionAbort));
1140    }
1141
1142    #[test]
1143    fn test_transaction_control_predicates() {
1144        assert!(PatchChange::TransactionBegin.is_transaction_control());
1145        assert!(PatchChange::TransactionCommit.is_transaction_control());
1146        assert!(PatchChange::TransactionAbort.is_transaction_control());
1147    }
1148
1149    // ── Prefix parsing ────────────────────────────────────────────────────
1150
1151    #[test]
1152    fn test_parse_prefix_add() {
1153        let patch = PatchParser::parse("PA ex <http://example.org/>\n").expect("should succeed");
1154        assert_eq!(patch.changes.len(), 1);
1155        match &patch.changes[0] {
1156            PatchChange::AddPrefix { prefix, iri } => {
1157                assert_eq!(prefix, "ex");
1158                assert_eq!(iri, "http://example.org/");
1159            }
1160            _ => panic!("unexpected change type"),
1161        }
1162    }
1163
1164    #[test]
1165    fn test_parse_prefix_delete() {
1166        let patch = PatchParser::parse("PD ex <http://example.org/>\n").expect("should succeed");
1167        assert!(
1168            matches!(&patch.changes[0], PatchChange::DeletePrefix { prefix, .. } if prefix == "ex")
1169        );
1170    }
1171
1172    #[test]
1173    fn test_prefix_resolution_in_triple() {
1174        let input = "PA ex <http://example.org/>\nA ex:s ex:p ex:o .\n";
1175        let patch = PatchParser::parse(input).expect("should succeed");
1176        assert_eq!(patch.changes.len(), 2);
1177        if let PatchChange::AddTriple(t) = &patch.changes[1] {
1178            assert_eq!(t.subject.value(), "http://example.org/s");
1179        } else {
1180            panic!("expected AddTriple");
1181        }
1182    }
1183
1184    // ── Triple operations ─────────────────────────────────────────────────
1185
1186    #[test]
1187    fn test_parse_add_triple() {
1188        let input = "A <http://s> <http://p> <http://o> .\n";
1189        let patch = PatchParser::parse(input).expect("should succeed");
1190        assert!(matches!(&patch.changes[0], PatchChange::AddTriple(_)));
1191    }
1192
1193    #[test]
1194    fn test_parse_delete_triple() {
1195        let input = "D <http://s> <http://p> <http://o> .\n";
1196        let patch = PatchParser::parse(input).expect("should succeed");
1197        assert!(matches!(&patch.changes[0], PatchChange::DeleteTriple(_)));
1198    }
1199
1200    #[test]
1201    fn test_parse_triple_with_literal() {
1202        let input = "A <http://s> <http://p> \"hello\" .\n";
1203        let patch = PatchParser::parse(input).expect("should succeed");
1204        if let PatchChange::AddTriple(t) = &patch.changes[0] {
1205            assert!(
1206                t.object.0.term_type
1207                    == TermType::Literal {
1208                        datatype: None,
1209                        lang: None
1210                    }
1211            );
1212            assert_eq!(t.object.value(), "hello");
1213        } else {
1214            panic!("expected AddTriple");
1215        }
1216    }
1217
1218    #[test]
1219    fn test_parse_literal_with_language() {
1220        let input = "A <http://s> <http://p> \"hello\"@en .\n";
1221        let patch = PatchParser::parse(input).expect("should succeed");
1222        if let PatchChange::AddTriple(t) = &patch.changes[0] {
1223            assert!(matches!(
1224                &t.object.0.term_type,
1225                TermType::Literal { lang: Some(l), .. } if l == "en"
1226            ));
1227        } else {
1228            panic!("expected AddTriple");
1229        }
1230    }
1231
1232    #[test]
1233    fn test_parse_literal_with_datatype() {
1234        let input =
1235            "A <http://s> <http://p> \"42\"^^<http://www.w3.org/2001/XMLSchema#integer> .\n";
1236        let patch = PatchParser::parse(input).expect("should succeed");
1237        if let PatchChange::AddTriple(t) = &patch.changes[0] {
1238            assert!(matches!(
1239                &t.object.0.term_type,
1240                TermType::Literal { datatype: Some(dt), .. }
1241                if dt == "http://www.w3.org/2001/XMLSchema#integer"
1242            ));
1243        } else {
1244            panic!("expected AddTriple");
1245        }
1246    }
1247
1248    #[test]
1249    fn test_parse_triple_blank_node() {
1250        let input = "A _:b0 <http://p> <http://o> .\n";
1251        let patch = PatchParser::parse(input).expect("should succeed");
1252        if let PatchChange::AddTriple(t) = &patch.changes[0] {
1253            assert!(t.subject.is_blank_node());
1254            assert_eq!(t.subject.value(), "b0");
1255        } else {
1256            panic!("expected AddTriple");
1257        }
1258    }
1259
1260    // ── Quad operations ───────────────────────────────────────────────────
1261
1262    #[test]
1263    fn test_parse_add_quad() {
1264        let input = "A <http://s> <http://p> <http://o> <http://g> .\n";
1265        let patch = PatchParser::parse(input).expect("should succeed");
1266        assert!(matches!(&patch.changes[0], PatchChange::AddQuad(_)));
1267    }
1268
1269    #[test]
1270    fn test_parse_delete_quad() {
1271        let input = "D <http://s> <http://p> <http://o> <http://g> .\n";
1272        let patch = PatchParser::parse(input).expect("should succeed");
1273        assert!(matches!(&patch.changes[0], PatchChange::DeleteQuad(_)));
1274    }
1275
1276    #[test]
1277    fn test_quad_graph_term() {
1278        let input = "A <http://s> <http://p> <http://o> <http://graph1> .\n";
1279        let patch = PatchParser::parse(input).expect("should succeed");
1280        if let PatchChange::AddQuad(q) = &patch.changes[0] {
1281            assert_eq!(q.graph.value(), "http://graph1");
1282        } else {
1283            panic!("expected AddQuad");
1284        }
1285    }
1286
1287    // ── Serialization ─────────────────────────────────────────────────────
1288
1289    #[test]
1290    fn test_serialize_header() {
1291        let patch = RdfPatch {
1292            headers: vec![PatchHeader::Id("urn:1".to_string())],
1293            changes: vec![],
1294        };
1295        let s = PatchSerializer::serialize(&patch);
1296        assert!(s.contains("H id urn:1"));
1297    }
1298
1299    #[test]
1300    fn test_serialize_add_triple() {
1301        let patch = RdfPatch {
1302            headers: vec![],
1303            changes: vec![PatchChange::AddTriple(triple(
1304                "http://s", "http://p", "http://o",
1305            ))],
1306        };
1307        let s = PatchSerializer::serialize(&patch);
1308        assert!(s.contains("A <http://s> <http://p> <http://o>"));
1309    }
1310
1311    #[test]
1312    fn test_serialize_delete_triple() {
1313        let patch = RdfPatch {
1314            headers: vec![],
1315            changes: vec![PatchChange::DeleteTriple(triple(
1316                "http://s", "http://p", "http://o",
1317            ))],
1318        };
1319        let s = PatchSerializer::serialize(&patch);
1320        assert!(s.starts_with("D "));
1321    }
1322
1323    #[test]
1324    fn test_serialize_prefix_add() {
1325        let change = PatchChange::AddPrefix {
1326            prefix: "ex".to_string(),
1327            iri: "http://example.org/".to_string(),
1328        };
1329        let s = PatchSerializer::serialize_change(&change);
1330        assert_eq!(s, "PA ex <http://example.org/>");
1331    }
1332
1333    #[test]
1334    fn test_serialize_transaction_control() {
1335        let patch = RdfPatch {
1336            headers: vec![],
1337            changes: vec![
1338                PatchChange::TransactionBegin,
1339                PatchChange::TransactionCommit,
1340            ],
1341        };
1342        let s = PatchSerializer::serialize(&patch);
1343        assert!(s.contains("TX"));
1344        assert!(s.contains("TC"));
1345    }
1346
1347    #[test]
1348    fn test_serialize_literal() {
1349        let patch = RdfPatch {
1350            headers: vec![],
1351            changes: vec![PatchChange::AddTriple(triple_lit(
1352                "http://s", "http://p", "hello",
1353            ))],
1354        };
1355        let s = PatchSerializer::serialize(&patch);
1356        assert!(s.contains("\"hello\""));
1357    }
1358
1359    // ── Apply patch ───────────────────────────────────────────────────────
1360
1361    #[test]
1362    fn test_apply_add_triple() {
1363        let mut graph = Graph::new();
1364        let patch = RdfPatch {
1365            headers: vec![],
1366            changes: vec![PatchChange::AddTriple(triple(
1367                "http://s", "http://p", "http://o",
1368            ))],
1369        };
1370        let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1371        assert_eq!(stats.triples_added, 1);
1372        assert_eq!(graph.len(), 1);
1373    }
1374
1375    #[test]
1376    fn test_apply_delete_triple() {
1377        let mut graph = Graph::new();
1378        let t = triple("http://s", "http://p", "http://o");
1379        graph.add_triple(t.clone());
1380        let patch = RdfPatch {
1381            headers: vec![],
1382            changes: vec![PatchChange::DeleteTriple(t)],
1383        };
1384        let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1385        assert_eq!(stats.triples_deleted, 1);
1386        assert_eq!(graph.len(), 0);
1387    }
1388
1389    #[test]
1390    fn test_apply_idempotent_add() {
1391        let mut graph = Graph::new();
1392        let t = triple("http://s", "http://p", "http://o");
1393        graph.add_triple(t.clone());
1394        let patch = RdfPatch {
1395            headers: vec![],
1396            changes: vec![PatchChange::AddTriple(t)],
1397        };
1398        let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1399        // Should not double-count
1400        assert_eq!(stats.triples_added, 0);
1401        assert_eq!(graph.len(), 1);
1402    }
1403
1404    #[test]
1405    fn test_apply_prefix_add() {
1406        let mut graph = Graph::new();
1407        let patch = RdfPatch {
1408            headers: vec![],
1409            changes: vec![PatchChange::AddPrefix {
1410                prefix: "ex".to_string(),
1411                iri: "http://example.org/".to_string(),
1412            }],
1413        };
1414        let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1415        assert_eq!(stats.prefixes_added, 1);
1416        assert_eq!(
1417            graph.prefixes.get("ex").map(String::as_str),
1418            Some("http://example.org/")
1419        );
1420    }
1421
1422    #[test]
1423    fn test_apply_transaction_commit() {
1424        let mut graph = Graph::new();
1425        let patch = RdfPatch {
1426            headers: vec![],
1427            changes: vec![
1428                PatchChange::TransactionBegin,
1429                PatchChange::AddTriple(triple("http://s", "http://p", "http://o")),
1430                PatchChange::TransactionCommit,
1431            ],
1432        };
1433        let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1434        assert_eq!(stats.triples_added, 1);
1435        assert_eq!(stats.transactions, 1);
1436        assert_eq!(graph.len(), 1);
1437    }
1438
1439    #[test]
1440    fn test_apply_transaction_abort() {
1441        let mut graph = Graph::new();
1442        let patch = RdfPatch {
1443            headers: vec![],
1444            changes: vec![
1445                PatchChange::TransactionBegin,
1446                PatchChange::AddTriple(triple("http://s", "http://p", "http://o")),
1447                PatchChange::TransactionAbort,
1448            ],
1449        };
1450        let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1451        assert_eq!(stats.aborts, 1);
1452        // Graph must remain empty — abort rolls back staged changes
1453        assert_eq!(graph.len(), 0);
1454    }
1455
1456    #[test]
1457    fn test_apply_multiple_changes() {
1458        let mut graph = Graph::new();
1459        let t1 = triple("http://a", "http://p", "http://x");
1460        let t2 = triple("http://b", "http://p", "http://y");
1461        let patch = RdfPatch {
1462            headers: vec![],
1463            changes: vec![
1464                PatchChange::AddTriple(t1.clone()),
1465                PatchChange::AddTriple(t2.clone()),
1466                PatchChange::DeleteTriple(t1),
1467            ],
1468        };
1469        let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1470        assert_eq!(stats.triples_added, 2);
1471        assert_eq!(stats.triples_deleted, 1);
1472        assert_eq!(graph.len(), 1);
1473    }
1474
1475    // ── Graph diff ────────────────────────────────────────────────────────
1476
1477    #[test]
1478    fn test_diff_to_patch_add() {
1479        let old = Graph::new();
1480        let mut new_graph = Graph::new();
1481        new_graph.add_triple(triple("http://s", "http://p", "http://o"));
1482        let patch = diff_to_patch(&old, &new_graph);
1483        assert_eq!(patch.add_count(), 1);
1484        assert_eq!(patch.delete_count(), 0);
1485    }
1486
1487    #[test]
1488    fn test_diff_to_patch_delete() {
1489        let mut old = Graph::new();
1490        old.add_triple(triple("http://s", "http://p", "http://o"));
1491        let new_graph = Graph::new();
1492        let patch = diff_to_patch(&old, &new_graph);
1493        assert_eq!(patch.add_count(), 0);
1494        assert_eq!(patch.delete_count(), 1);
1495    }
1496
1497    #[test]
1498    fn test_diff_to_patch_no_change() {
1499        let mut old = Graph::new();
1500        old.add_triple(triple("http://s", "http://p", "http://o"));
1501        let new_graph = old.clone();
1502        let patch = diff_to_patch(&old, &new_graph);
1503        assert!(patch.changes.is_empty());
1504    }
1505
1506    #[test]
1507    fn test_diff_to_patch_prefix_added() {
1508        let old = Graph::new();
1509        let mut new_graph = Graph::new();
1510        new_graph
1511            .prefixes
1512            .insert("ex".to_string(), "http://example.org/".to_string());
1513        let patch = diff_to_patch(&old, &new_graph);
1514        assert!(patch
1515            .changes
1516            .iter()
1517            .any(|c| matches!(c, PatchChange::AddPrefix { .. })));
1518    }
1519
1520    #[test]
1521    fn test_diff_to_patch_prefix_removed() {
1522        let mut old = Graph::new();
1523        old.prefixes
1524            .insert("ex".to_string(), "http://example.org/".to_string());
1525        let new_graph = Graph::new();
1526        let patch = diff_to_patch(&old, &new_graph);
1527        assert!(patch
1528            .changes
1529            .iter()
1530            .any(|c| matches!(c, PatchChange::DeletePrefix { .. })));
1531    }
1532
1533    // ── Round-trip ────────────────────────────────────────────────────────
1534
1535    #[test]
1536    fn test_round_trip_simple() {
1537        let input = "H id <urn:1>\nA <http://s> <http://p> <http://o> .\nD <http://s> <http://p> <http://old> .\n";
1538        let patch = PatchParser::parse(input).expect("should succeed");
1539        let serialized = PatchSerializer::serialize(&patch);
1540        let reparsed = PatchParser::parse(&serialized).expect("should succeed");
1541        assert_eq!(reparsed.headers.len(), patch.headers.len());
1542        assert_eq!(reparsed.changes.len(), patch.changes.len());
1543    }
1544
1545    #[test]
1546    fn test_round_trip_with_prefixes() {
1547        let input = "PA ex <http://example.org/>\nA ex:s ex:p ex:o .\n";
1548        let patch = PatchParser::parse(input).expect("should succeed");
1549        let serialized = PatchSerializer::serialize(&patch);
1550        // After serialisation ex:s becomes <http://example.org/s>
1551        assert!(serialized.contains("<http://example.org/s>"));
1552        // Re-parse the serialised form
1553        let reparsed = PatchParser::parse(&serialized).expect("should succeed");
1554        assert_eq!(reparsed.changes.len(), 2);
1555    }
1556
1557    #[test]
1558    fn test_round_trip_transaction() {
1559        let input = "TX\nA <http://s> <http://p> <http://o> .\nTC\n";
1560        let patch = PatchParser::parse(input).expect("should succeed");
1561        let serialized = PatchSerializer::serialize(&patch);
1562        let reparsed = PatchParser::parse(&serialized).expect("should succeed");
1563        assert_eq!(reparsed.changes.len(), 3);
1564        assert!(matches!(reparsed.changes[0], PatchChange::TransactionBegin));
1565        assert!(matches!(
1566            reparsed.changes[2],
1567            PatchChange::TransactionCommit
1568        ));
1569    }
1570
1571    #[test]
1572    fn test_round_trip_with_blank_nodes() {
1573        let input = "A _:b0 <http://p> <http://o> .\n";
1574        let patch = PatchParser::parse(input).expect("should succeed");
1575        let s = PatchSerializer::serialize(&patch);
1576        let reparsed = PatchParser::parse(&s).expect("should succeed");
1577        if let PatchChange::AddTriple(t) = &reparsed.changes[0] {
1578            assert!(t.subject.is_blank_node());
1579        } else {
1580            panic!("expected AddTriple");
1581        }
1582    }
1583
1584    #[test]
1585    fn test_round_trip_literal_with_lang() {
1586        let input = "A <http://s> <http://p> \"bonjour\"@fr .\n";
1587        let patch = PatchParser::parse(input).expect("should succeed");
1588        let s = PatchSerializer::serialize(&patch);
1589        let reparsed = PatchParser::parse(&s).expect("should succeed");
1590        if let PatchChange::AddTriple(t) = &reparsed.changes[0] {
1591            assert!(matches!(
1592                &t.object.0.term_type,
1593                TermType::Literal { lang: Some(l), .. } if l == "fr"
1594            ));
1595        } else {
1596            panic!("expected AddTriple");
1597        }
1598    }
1599
1600    #[test]
1601    fn test_round_trip_literal_with_datatype() {
1602        let dt = "http://www.w3.org/2001/XMLSchema#integer";
1603        let input = format!("A <http://s> <http://p> \"42\"^^<{dt}> .\n");
1604        let patch = PatchParser::parse(&input).expect("should succeed");
1605        let s = PatchSerializer::serialize(&patch);
1606        let reparsed = PatchParser::parse(&s).expect("should succeed");
1607        if let PatchChange::AddTriple(t) = &reparsed.changes[0] {
1608            assert!(matches!(
1609                &t.object.0.term_type,
1610                TermType::Literal { datatype: Some(d), .. } if d == dt
1611            ));
1612        } else {
1613            panic!("expected AddTriple");
1614        }
1615    }
1616
1617    // ── Streaming parser ──────────────────────────────────────────────────
1618
1619    #[test]
1620    fn test_streaming_parser_basic() {
1621        let input = "TX\nA <http://s> <http://p> <http://o> .\nTC\n";
1622        let changes: Vec<_> = PatchParser::parse_streaming(input.as_bytes()).collect();
1623        assert_eq!(changes.len(), 3);
1624        assert!(changes[0]
1625            .as_ref()
1626            .map(|c| matches!(c, PatchChange::TransactionBegin))
1627            .unwrap_or(false));
1628    }
1629
1630    #[test]
1631    fn test_streaming_skips_headers() {
1632        let input = "H id <urn:1>\nA <http://s> <http://p> <http://o> .\n";
1633        let changes: Vec<_> = PatchParser::parse_streaming(input.as_bytes()).collect();
1634        // Header is skipped in streaming mode
1635        assert_eq!(changes.len(), 1);
1636    }
1637
1638    #[test]
1639    fn test_streaming_parser_prefixes() {
1640        let input = "PA ex <http://example.org/>\nA ex:s ex:p ex:o .\n";
1641        let changes: Vec<_> = PatchParser::parse_streaming(input.as_bytes())
1642            .collect::<Result<Vec<_>, _>>()
1643            .expect("should succeed");
1644        assert_eq!(changes.len(), 2);
1645    }
1646
1647    #[test]
1648    fn test_streaming_parser_multiple_batches() {
1649        let input = "A <http://s1> <http://p> <http://o1> .\nA <http://s2> <http://p> <http://o2> .\nD <http://s1> <http://p> <http://o1> .\n";
1650        let changes: Vec<_> = PatchParser::parse_streaming(input.as_bytes())
1651            .collect::<Result<Vec<_>, _>>()
1652            .expect("should succeed");
1653        assert_eq!(changes.len(), 3);
1654    }
1655
1656    // ── Edge cases ────────────────────────────────────────────────────────
1657
1658    #[test]
1659    fn test_empty_patch() {
1660        let patch = PatchParser::parse("").expect("should succeed");
1661        assert!(patch.is_empty());
1662    }
1663
1664    #[test]
1665    fn test_comments_ignored() {
1666        let input =
1667            "# This is a comment\nA <http://s> <http://p> <http://o> .\n# Another comment\n";
1668        let patch = PatchParser::parse(input).expect("should succeed");
1669        assert_eq!(patch.changes.len(), 1);
1670    }
1671
1672    #[test]
1673    fn test_blank_lines_ignored() {
1674        let input = "\n\nA <http://s> <http://p> <http://o> .\n\n";
1675        let patch = PatchParser::parse(input).expect("should succeed");
1676        assert_eq!(patch.changes.len(), 1);
1677    }
1678
1679    #[test]
1680    fn test_error_unknown_line() {
1681        let result = PatchParser::parse("UNKNOWN_CMD <http://x>\n");
1682        assert!(result.is_err());
1683    }
1684
1685    #[test]
1686    fn test_error_unterminated_iri() {
1687        let result = PatchParser::parse("A <http://s <http://p> <http://o> .\n");
1688        assert!(result.is_err());
1689    }
1690
1691    #[test]
1692    fn test_patch_change_predicates() {
1693        assert!(PatchChange::AddTriple(triple("h://s", "h://p", "h://o")).is_add());
1694        assert!(PatchChange::DeleteTriple(triple("h://s", "h://p", "h://o")).is_delete());
1695        assert!(!PatchChange::AddTriple(triple("h://s", "h://p", "h://o")).is_delete());
1696        assert!(!PatchChange::DeleteTriple(triple("h://s", "h://p", "h://o")).is_add());
1697    }
1698
1699    #[test]
1700    fn test_graph_contains() {
1701        let mut g = Graph::new();
1702        let t = triple("http://s", "http://p", "http://o");
1703        assert!(!g.contains(&t));
1704        g.add_triple(t.clone());
1705        assert!(g.contains(&t));
1706        g.remove_triple(&t);
1707        assert!(!g.contains(&t));
1708    }
1709
1710    #[test]
1711    fn test_graph_len() {
1712        let mut g = Graph::new();
1713        assert_eq!(g.len(), 0);
1714        assert!(g.is_empty());
1715        g.add_triple(triple("http://s", "http://p", "http://o"));
1716        assert_eq!(g.len(), 1);
1717        assert!(!g.is_empty());
1718    }
1719
1720    #[test]
1721    fn test_apply_patch_from_parsed_text() {
1722        let input =
1723            "PA ex <http://example.org/>\nTX\nA ex:alice <http://type> <http://Person> .\nTC\n";
1724        let patch = PatchParser::parse(input).expect("should succeed");
1725        let mut graph = Graph::new();
1726        let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1727        assert_eq!(stats.triples_added, 1);
1728        assert_eq!(stats.transactions, 1);
1729    }
1730
1731    #[test]
1732    fn test_patch_stats_default() {
1733        let stats = PatchStats::default();
1734        assert_eq!(stats.triples_added, 0);
1735        assert_eq!(stats.triples_deleted, 0);
1736        assert_eq!(stats.prefixes_added, 0);
1737        assert_eq!(stats.prefixes_deleted, 0);
1738        assert_eq!(stats.transactions, 0);
1739        assert_eq!(stats.aborts, 0);
1740    }
1741
1742    #[test]
1743    fn test_patch_header_key_value() {
1744        let h = PatchHeader::Id("urn:test".to_string());
1745        assert_eq!(h.key(), "id");
1746        assert_eq!(h.value(), "urn:test");
1747    }
1748
1749    #[test]
1750    fn test_diff_then_apply_round_trip() {
1751        let mut old = Graph::new();
1752        old.add_triple(triple("http://s", "http://p", "http://o1"));
1753        old.add_triple(triple("http://s", "http://p", "http://o2"));
1754
1755        let mut new_graph = Graph::new();
1756        new_graph.add_triple(triple("http://s", "http://p", "http://o2"));
1757        new_graph.add_triple(triple("http://s", "http://p", "http://o3"));
1758
1759        let patch = diff_to_patch(&old, &new_graph);
1760        // Apply patch to old to get new
1761        let mut result = old.clone();
1762        apply_patch(&mut result, &patch).expect("should succeed");
1763
1764        assert_eq!(result.len(), new_graph.len());
1765        for t in new_graph.iter() {
1766            assert!(result.contains(t), "missing triple: {t}");
1767        }
1768    }
1769
1770    #[test]
1771    fn test_serialize_then_parse_complete_patch() {
1772        let mut patch = RdfPatch::new();
1773        patch
1774            .headers
1775            .push(PatchHeader::Id("urn:test:42".to_string()));
1776        patch
1777            .headers
1778            .push(PatchHeader::Previous("urn:test:41".to_string()));
1779        patch.changes.push(PatchChange::AddPrefix {
1780            prefix: "foaf".to_string(),
1781            iri: "http://xmlns.com/foaf/0.1/".to_string(),
1782        });
1783        patch.changes.push(PatchChange::TransactionBegin);
1784        patch.changes.push(PatchChange::AddTriple(triple(
1785            "http://example.org/alice",
1786            "http://xmlns.com/foaf/0.1/name",
1787            "http://example.org/literal_placeholder",
1788        )));
1789        patch.changes.push(PatchChange::TransactionCommit);
1790
1791        let serialized = PatchSerializer::serialize(&patch);
1792        let reparsed = PatchParser::parse(&serialized).expect("should succeed");
1793
1794        assert_eq!(reparsed.id(), Some("urn:test:42"));
1795        assert_eq!(reparsed.previous(), Some("urn:test:41"));
1796        assert_eq!(reparsed.changes.len(), 4);
1797    }
1798
1799    #[test]
1800    fn test_quad_apply_to_simple_graph() {
1801        let mut graph = Graph::new();
1802        let q = PatchQuad::new(
1803            PatchTerm::iri("http://s"),
1804            PatchTerm::iri("http://p"),
1805            PatchTerm::iri("http://o"),
1806            PatchTerm::iri("http://graph1"),
1807        );
1808        let patch = RdfPatch {
1809            headers: vec![],
1810            changes: vec![PatchChange::AddQuad(q)],
1811        };
1812        let stats = apply_patch(&mut graph, &patch).expect("should succeed");
1813        assert_eq!(stats.triples_added, 1);
1814    }
1815
1816    #[test]
1817    fn test_escaped_literal() {
1818        let input = "A <http://s> <http://p> \"say \\\"hello\\\"\" .\n";
1819        let patch = PatchParser::parse(input).expect("should succeed");
1820        if let PatchChange::AddTriple(t) = &patch.changes[0] {
1821            assert_eq!(t.object.value(), "say \"hello\"");
1822        } else {
1823            panic!("expected AddTriple");
1824        }
1825    }
1826
1827    #[test]
1828    fn test_newline_in_literal_escape() {
1829        let input = "A <http://s> <http://p> \"line1\\nline2\" .\n";
1830        let patch = PatchParser::parse(input).expect("should succeed");
1831        if let PatchChange::AddTriple(t) = &patch.changes[0] {
1832            assert!(t.object.value().contains('\n'));
1833        } else {
1834            panic!("expected AddTriple");
1835        }
1836    }
1837
1838    #[test]
1839    fn test_patch_change_line_prefix() {
1840        assert_eq!(PatchChange::TransactionBegin.line_prefix(), "TX");
1841        assert_eq!(PatchChange::TransactionCommit.line_prefix(), "TC");
1842        assert_eq!(PatchChange::TransactionAbort.line_prefix(), "TA");
1843        assert_eq!(
1844            PatchChange::AddTriple(triple("http://s", "http://p", "http://o")).line_prefix(),
1845            "A"
1846        );
1847        assert_eq!(
1848            PatchChange::DeleteTriple(triple("http://s", "http://p", "http://o")).line_prefix(),
1849            "D"
1850        );
1851    }
1852}