oxirs-tdb 0.4.1

Apache Jena TDB/TDB2 compatible RDF storage engine with B+Tree indexes
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
//! Named-graph (quad) API for the TDB store (F4).
//!
//! A dataset is a default (unnamed) graph plus zero or more named graphs. This
//! module layers a quad API over the store:
//!
//! - **Default graph** quads are stored in the existing SPO/POS/OSP triple
//!   indexes, so the triple API ([`insert`](crate::store::TdbStore::insert),
//!   [`query_triples`](crate::store::TdbStore::query_triples), …) and the
//!   default-graph quad API operate on the same data and both round-trip
//!   through reopen exactly as before.
//! - **Named graphs** are stored in the GSPO/GPOS/GOSP quad indexes
//!   ([`QuadIndexes`]); the graph name is interned in
//!   the shared dictionary just like any subject/predicate/object term (this is
//!   the "graph column" — a [`Term`] is a [`Term`], so no separate dictionary is
//!   needed).
//!
//! Quad-index roots and the named-graph quad count are persisted in the
//! superblock and fsynced on [`sync`](crate::store::TdbStore::sync), so quads
//! survive a `drop` + reopen just like triples.

use crate::dictionary::{Dictionary, Term};
use crate::error::{Result, TdbError};
use crate::index::{Quad, QuadIndexes, QuadScan, Triple, TripleScan};
use crate::store::store_impl::TdbStore;
use crate::store::store_stream::decode_triple_terms;
use crate::store::store_wal::StoreOp;

/// The graph a decoded quad belongs to.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum GraphName {
    /// The unnamed default graph (backed by the triple indexes).
    DefaultGraph,
    /// A named graph identified by a [`Term`] (an IRI or blank node).
    Named(Term),
}

/// Selects which graph(s) a quad scan targets.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphTarget<'a> {
    /// Match quads in any graph — the default graph and every named graph.
    AnyGraph,
    /// Match quads only in the default (unnamed) graph.
    DefaultGraph,
    /// Match quads only in the named graph identified by this term.
    Named(&'a Term),
}

/// A decoded quad returned by the quad scan API.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct QuadResult {
    /// The graph the quad belongs to.
    pub graph: GraphName,
    /// Subject term.
    pub subject: Term,
    /// Predicate term.
    pub predicate: Term,
    /// Object term.
    pub object: Term,
}

/// Decode a node-encoded named-graph [`Quad`] into a [`QuadResult`], failing
/// loudly if any id (including the graph id) is absent from the dictionary.
fn decode_quad_terms(dictionary: &Dictionary, quad: Quad) -> Result<QuadResult> {
    let graph = dictionary
        .decode(quad.graph)?
        .ok_or_else(|| TdbError::Other("Graph id not found in dictionary".to_string()))?;
    let subject = dictionary
        .decode(quad.subject)?
        .ok_or_else(|| TdbError::Other("Subject id not found in dictionary".to_string()))?;
    let predicate = dictionary
        .decode(quad.predicate)?
        .ok_or_else(|| TdbError::Other("Predicate id not found in dictionary".to_string()))?;
    let object = dictionary
        .decode(quad.object)?
        .ok_or_else(|| TdbError::Other("Object id not found in dictionary".to_string()))?;
    Ok(QuadResult {
        graph: GraphName::Named(graph),
        subject,
        predicate,
        object,
    })
}

/// A lazy, streaming iterator over decoded quads (F5).
///
/// Created by [`TdbStore::quad_iter`]. It first drains the default-graph part
/// (a triple scan, tagged [`GraphName::DefaultGraph`]) and then the named-graph
/// part (a quad scan, tagged [`GraphName::Named`]); either part may be absent
/// depending on the [`GraphTarget`]. The full result set is never materialized
/// in memory at once — only the underlying B+Tree leaf pages are buffered by
/// the scans.
pub struct QuadTermIter<'a> {
    /// Dictionary used to decode node ids back into terms.
    dictionary: &'a Dictionary,
    /// Default-graph triple scan (yields [`GraphName::DefaultGraph`] quads).
    default_scan: Option<TripleScan>,
    /// Named-graph quad scan (yields [`GraphName::Named`] quads).
    named_scan: Option<QuadScan>,
}

impl<'a> QuadTermIter<'a> {
    /// An iterator that yields nothing (used when a pattern term is unknown).
    fn empty(dictionary: &'a Dictionary) -> Self {
        Self {
            dictionary,
            default_scan: None,
            named_scan: None,
        }
    }
}

impl Iterator for QuadTermIter<'_> {
    type Item = Result<QuadResult>;

    fn next(&mut self) -> Option<Self::Item> {
        // Drain the default-graph part first.
        if let Some(scan) = self.default_scan.as_mut() {
            match scan.next() {
                Some(Ok(triple)) => {
                    return Some(decode_triple_terms(self.dictionary, triple).map(
                        |(subject, predicate, object)| QuadResult {
                            graph: GraphName::DefaultGraph,
                            subject,
                            predicate,
                            object,
                        },
                    ));
                }
                Some(Err(e)) => return Some(Err(e)),
                None => self.default_scan = None,
            }
        }

        // Then the named-graph part.
        if let Some(scan) = self.named_scan.as_mut() {
            match scan.next() {
                Some(Ok(quad)) => return Some(decode_quad_terms(self.dictionary, quad)),
                Some(Err(e)) => return Some(Err(e)),
                None => self.named_scan = None,
            }
        }

        None
    }
}

impl TdbStore {
    /// Number of named-graph quads currently stored.
    ///
    /// Default-graph triples are counted separately by
    /// [`count`](crate::store::TdbStore::count); the whole-dataset size is
    /// [`dataset_len`](TdbStore::dataset_len).
    pub fn quad_count(&self) -> usize {
        self.quad_count
    }

    /// Total number of statements across the whole dataset: default-graph
    /// triples plus named-graph quads.
    pub fn dataset_len(&self) -> usize {
        self.triple_count + self.quad_count
    }

    /// Whether named-graph (quad) support is available on this store handle.
    pub fn quads_enabled(&self) -> bool {
        self.quad_indexes.is_some()
    }

    /// Insert a quad `(graph, subject, predicate, object)`.
    ///
    /// `graph` is `None` for the default graph (routed to the triple indexes)
    /// or `Some(term)` for a named graph (routed to the quad indexes, interning
    /// the graph term). Returns `true` if the quad was newly added, `false` if
    /// it already existed. Named-graph inserts fail with
    /// [`TdbError::Unsupported`] when quad support is disabled.
    pub fn insert_quad(
        &mut self,
        graph: Option<&Term>,
        subject: &Term,
        predicate: &Term,
        object: &Term,
    ) -> Result<bool> {
        match graph {
            None => self.insert_default_graph_quad(subject, predicate, object),
            Some(graph_term) => {
                if !self.quads_writable {
                    return Err(TdbError::Unsupported(
                        "named-graph writes are disabled (open with enable_quad_indexes = true)"
                            .to_string(),
                    ));
                }
                let g_id = self.dictionary.encode(graph_term)?;
                let s_id = self.dictionary.encode(subject)?;
                let p_id = self.dictionary.encode(predicate)?;
                let o_id = self.dictionary.encode(object)?;
                let quad = Quad::new(g_id, s_id, p_id, o_id);
                let is_new = {
                    let quad_indexes = self.quad_indexes.as_mut().ok_or_else(|| {
                        TdbError::Unsupported("quad indexes are not initialized".to_string())
                    })?;
                    quad_indexes.insert(quad)?
                };
                if is_new {
                    self.quad_count += 1;
                    // Log the committed named-graph insert so it survives a crash
                    // before the next checkpoint.
                    self.wal_log_op(StoreOp::InsertQuad {
                        graph: graph_term.clone(),
                        subject: subject.clone(),
                        predicate: predicate.clone(),
                        object: object.clone(),
                    })?;
                }
                Ok(is_new)
            }
        }
    }

    /// Insert a default-graph quad into the triple indexes (shared with the
    /// triple API), maintaining the bloom filter and triple count.
    fn insert_default_graph_quad(
        &mut self,
        subject: &Term,
        predicate: &Term,
        object: &Term,
    ) -> Result<bool> {
        let s_id = self.dictionary.encode(subject)?;
        let p_id = self.dictionary.encode(predicate)?;
        let o_id = self.dictionary.encode(object)?;
        let triple = Triple::new(s_id, p_id, o_id);
        let is_new = self.indexes.insert(triple)?;
        if let Some(ref mut bloom) = self.bloom_filter {
            bloom.insert(&triple);
        }
        if is_new {
            self.triple_count += 1;
            // A default-graph quad is a triple: log it as a triple insert (the
            // triple indexes back both the triple API and the default graph).
            self.wal_log_op(StoreOp::InsertTriple {
                subject: subject.clone(),
                predicate: predicate.clone(),
                object: object.clone(),
            })?;
        }
        // The default graph changed, so cached triple-query results are stale.
        self.query_cache.clear();
        Ok(is_new)
    }

    /// Bulk-insert quads `(graph, subject, predicate, object)` with a sorted,
    /// sequential-leaf-append B+Tree build (F6, Phase A).
    ///
    /// Default-graph quads (`graph == None`) are routed to the SPO/POS/OSP triple
    /// indexes; named-graph quads to the GSPO/GPOS/GOSP quad indexes. Each index
    /// is fed its batch pre-sorted in its own key order (see
    /// [`TripleIndexes::insert_sorted`](crate::index::TripleIndexes::insert_sorted)
    /// and [`QuadIndexes::insert_sorted`](crate::index::QuadIndexes::insert_sorted)),
    /// so the trees append to their right-most leaves instead of splitting in
    /// random order.
    ///
    /// Coordinating with F3, the whole batch is one WAL transaction and one
    /// checkpoint. Returns the number of genuinely new statements (default-graph
    /// triples plus named-graph quads) actually added.
    ///
    /// Fails loudly *before any mutation* if a subject is a literal, or if any
    /// named-graph quad is present while named-graph writes are disabled
    /// ([`TdbError::Unsupported`]).
    pub fn insert_quads_bulk(
        &mut self,
        quads: &[(Option<Term>, Term, Term, Term)],
    ) -> Result<usize> {
        // Validate the whole batch first so a malformed input never leaves a
        // half-applied store.
        let mut has_named = false;
        for (graph, subject, _predicate, _object) in quads {
            if matches!(subject, Term::Literal { .. }) {
                return Err(TdbError::Other("Subject cannot be a literal".to_string()));
            }
            if graph.is_some() {
                has_named = true;
            }
        }
        if has_named && !self.quads_writable {
            return Err(TdbError::Unsupported(
                "named-graph writes are disabled (open with enable_quad_indexes = true)"
                    .to_string(),
            ));
        }
        if quads.is_empty() {
            return Ok(0);
        }

        // (1)+(2) intern + encode, splitting default-graph triples from named
        // quads and building the one-transaction WAL op list in input order.
        let mut encoded_triples: Vec<Triple> = Vec::new();
        let mut encoded_quads: Vec<Quad> = Vec::new();
        let mut ops: Vec<StoreOp> = Vec::with_capacity(quads.len());
        for (graph, subject, predicate, object) in quads {
            let s_id = self.dictionary.encode(subject)?;
            let p_id = self.dictionary.encode(predicate)?;
            let o_id = self.dictionary.encode(object)?;
            match graph {
                None => {
                    encoded_triples.push(Triple::new(s_id, p_id, o_id));
                    ops.push(StoreOp::InsertTriple {
                        subject: subject.clone(),
                        predicate: predicate.clone(),
                        object: object.clone(),
                    });
                }
                Some(graph_term) => {
                    let g_id = self.dictionary.encode(graph_term)?;
                    encoded_quads.push(Quad::new(g_id, s_id, p_id, o_id));
                    ops.push(StoreOp::InsertQuad {
                        graph: graph_term.clone(),
                        subject: subject.clone(),
                        predicate: predicate.clone(),
                        object: object.clone(),
                    });
                }
            }
        }

        // (3)+(4) sorted bulk build for the default-graph triple indexes.
        let mut new_count = self.indexes.insert_sorted(&encoded_triples)?;
        if let Some(ref mut bloom) = self.bloom_filter {
            for triple in &encoded_triples {
                bloom.insert(triple);
            }
        }
        self.triple_count += new_count;

        // Sorted bulk build for the named-graph quad indexes (materialized on
        // demand — only reachable when quads are writable, guarded above).
        if !encoded_quads.is_empty() {
            if self.quad_indexes.is_none() {
                self.quad_indexes = Some(QuadIndexes::new(self.buffer_pool.clone()));
                self.quads_writable = true;
            }
            let quad_indexes = self.quad_indexes.as_mut().ok_or_else(|| {
                TdbError::Unsupported("quad indexes are not initialized".to_string())
            })?;
            let new_quads = quad_indexes.insert_sorted(&encoded_quads)?;
            self.quad_count += new_quads;
            new_count += new_quads;
        }

        // The default graph changed, so cached triple-query results are stale.
        self.query_cache.clear();

        // One WAL transaction + one durable checkpoint for the whole batch (F3).
        self.wal_log_batch(&ops)?;
        self.sync()?;

        Ok(new_count)
    }

    /// Delete a quad `(graph, subject, predicate, object)`.
    ///
    /// Returns `true` if a quad was removed. Returns `Ok(false)` (not an error)
    /// when the quad — or any of its terms — is not present. Named-graph deletes
    /// fail with [`TdbError::Unsupported`] when quad support is disabled.
    pub fn delete_quad(
        &mut self,
        graph: Option<&Term>,
        subject: &Term,
        predicate: &Term,
        object: &Term,
    ) -> Result<bool> {
        match graph {
            None => {
                let (s_id, p_id, o_id) = match self.lookup_spo(subject, predicate, object)? {
                    Some(ids) => ids,
                    None => return Ok(false),
                };
                let triple = Triple::new(s_id, p_id, o_id);
                let deleted = self.indexes.delete(&triple)?;
                if deleted {
                    self.triple_count = self.triple_count.saturating_sub(1);
                    self.query_cache.clear();
                    // A default-graph quad delete is a triple delete.
                    self.wal_log_op(StoreOp::DeleteTriple {
                        subject: subject.clone(),
                        predicate: predicate.clone(),
                        object: object.clone(),
                    })?;
                }
                Ok(deleted)
            }
            Some(graph_term) => {
                if !self.quads_writable {
                    return Err(TdbError::Unsupported(
                        "named-graph writes are disabled (open with enable_quad_indexes = true)"
                            .to_string(),
                    ));
                }
                let g_id = match self.dictionary.lookup(graph_term)? {
                    Some(id) => id,
                    None => return Ok(false),
                };
                let (s_id, p_id, o_id) = match self.lookup_spo(subject, predicate, object)? {
                    Some(ids) => ids,
                    None => return Ok(false),
                };
                let quad = Quad::new(g_id, s_id, p_id, o_id);
                let deleted = {
                    let quad_indexes = self.quad_indexes.as_mut().ok_or_else(|| {
                        TdbError::Unsupported("quad indexes are not initialized".to_string())
                    })?;
                    quad_indexes.delete(quad)?
                };
                if deleted {
                    self.quad_count = self.quad_count.saturating_sub(1);
                    // Log the committed named-graph delete.
                    self.wal_log_op(StoreOp::DeleteQuad {
                        graph: graph_term.clone(),
                        subject: subject.clone(),
                        predicate: predicate.clone(),
                        object: object.clone(),
                    })?;
                }
                Ok(deleted)
            }
        }
    }

    /// Check whether a quad `(graph, subject, predicate, object)` exists.
    pub fn contains_quad(
        &self,
        graph: Option<&Term>,
        subject: &Term,
        predicate: &Term,
        object: &Term,
    ) -> Result<bool> {
        match graph {
            None => {
                let (s_id, p_id, o_id) = match self.lookup_spo(subject, predicate, object)? {
                    Some(ids) => ids,
                    None => return Ok(false),
                };
                self.indexes.contains(&Triple::new(s_id, p_id, o_id))
            }
            Some(graph_term) => {
                let g_id = match self.dictionary.lookup(graph_term)? {
                    Some(id) => id,
                    None => return Ok(false),
                };
                let (s_id, p_id, o_id) = match self.lookup_spo(subject, predicate, object)? {
                    Some(ids) => ids,
                    None => return Ok(false),
                };
                match &self.quad_indexes {
                    Some(quad_indexes) => quad_indexes.contains(&Quad::new(g_id, s_id, p_id, o_id)),
                    None => Ok(false),
                }
            }
        }
    }

    /// Look up the node ids of a `(subject, predicate, object)` triple, or
    /// `None` if any term is absent from the dictionary.
    fn lookup_spo(
        &self,
        subject: &Term,
        predicate: &Term,
        object: &Term,
    ) -> Result<
        Option<(
            crate::dictionary::NodeId,
            crate::dictionary::NodeId,
            crate::dictionary::NodeId,
        )>,
    > {
        let s_id = match self.dictionary.lookup(subject)? {
            Some(id) => id,
            None => return Ok(None),
        };
        let p_id = match self.dictionary.lookup(predicate)? {
            Some(id) => id,
            None => return Ok(None),
        };
        let o_id = match self.dictionary.lookup(object)? {
            Some(id) => id,
            None => return Ok(None),
        };
        Ok(Some((s_id, p_id, o_id)))
    }

    /// Open a lazy, streaming iterator over the quads matching a pattern (F5).
    ///
    /// `graph` selects the default graph, a named graph, or any graph; each of
    /// `subject`/`predicate`/`object` is `None` for a wildcard. Decoded quads
    /// are produced one at a time without materializing the whole result set. A
    /// pattern component whose term is unknown yields an empty iterator.
    pub fn quad_iter(
        &self,
        graph: GraphTarget<'_>,
        subject: Option<&Term>,
        predicate: Option<&Term>,
        object: Option<&Term>,
    ) -> Result<QuadTermIter<'_>> {
        // Resolve pattern terms to ids; an unknown term matches nothing.
        let s_id = match self.resolve_pattern(subject)? {
            Some(id) => id,
            None => return Ok(QuadTermIter::empty(&self.dictionary)),
        };
        let p_id = match self.resolve_pattern(predicate)? {
            Some(id) => id,
            None => return Ok(QuadTermIter::empty(&self.dictionary)),
        };
        let o_id = match self.resolve_pattern(object)? {
            Some(id) => id,
            None => return Ok(QuadTermIter::empty(&self.dictionary)),
        };

        match graph {
            GraphTarget::DefaultGraph => {
                let default_scan = self.indexes.scan(s_id, p_id, o_id)?;
                Ok(QuadTermIter {
                    dictionary: &self.dictionary,
                    default_scan: Some(default_scan),
                    named_scan: None,
                })
            }
            GraphTarget::Named(graph_term) => {
                let g_id = match self.dictionary.lookup(graph_term)? {
                    Some(id) => id,
                    None => return Ok(QuadTermIter::empty(&self.dictionary)),
                };
                match &self.quad_indexes {
                    Some(quad_indexes) => {
                        let named_scan = quad_indexes.scan(Some(g_id), s_id, p_id, o_id)?;
                        Ok(QuadTermIter {
                            dictionary: &self.dictionary,
                            default_scan: None,
                            named_scan: Some(named_scan),
                        })
                    }
                    None => Ok(QuadTermIter::empty(&self.dictionary)),
                }
            }
            GraphTarget::AnyGraph => {
                let default_scan = Some(self.indexes.scan(s_id, p_id, o_id)?);
                let named_scan = match &self.quad_indexes {
                    Some(quad_indexes) => Some(quad_indexes.scan(None, s_id, p_id, o_id)?),
                    None => None,
                };
                Ok(QuadTermIter {
                    dictionary: &self.dictionary,
                    default_scan,
                    named_scan,
                })
            }
        }
    }

    /// Resolve an optional pattern term to `Some(Some(id))` when bound and
    /// present, `Some(None)` when a wildcard, and `None` when the term is bound
    /// but absent from the dictionary (so the whole scan yields nothing).
    fn resolve_pattern(
        &self,
        term: Option<&Term>,
    ) -> Result<Option<Option<crate::dictionary::NodeId>>> {
        match term {
            None => Ok(Some(None)),
            Some(t) => match self.dictionary.lookup(t)? {
                Some(id) => Ok(Some(Some(id))),
                None => Ok(None),
            },
        }
    }

    /// Scan quads matching a pattern, materializing the decoded results.
    ///
    /// A convenience `Vec`-collecting wrapper around [`TdbStore::quad_iter`];
    /// prefer `quad_iter` (or [`TdbStore::for_each_quad`]) for large result
    /// sets to avoid buffering everything in memory.
    pub fn scan_quads(
        &self,
        graph: GraphTarget<'_>,
        subject: Option<&Term>,
        predicate: Option<&Term>,
        object: Option<&Term>,
    ) -> Result<Vec<QuadResult>> {
        self.quad_iter(graph, subject, predicate, object)?.collect()
    }

    /// Invoke `f` for each quad matching the pattern, streaming (never
    /// materializing the whole result). Stops early and propagates the error if
    /// `f` returns `Err`.
    pub fn for_each_quad<F>(
        &self,
        graph: GraphTarget<'_>,
        subject: Option<&Term>,
        predicate: Option<&Term>,
        object: Option<&Term>,
        mut f: F,
    ) -> Result<()>
    where
        F: FnMut(QuadResult) -> Result<()>,
    {
        for item in self.quad_iter(graph, subject, predicate, object)? {
            f(item?)?;
        }
        Ok(())
    }
}