Repository navigation
Architecture
OntoDB is a SPARQL store. A query is a pipeline of small types. A committed insert is the same pipeline plus the log. The files below are the ones each path actually touches.
Take this, typed at the OntoDB> prompt after \load data/tiny.ttl:
PREFIX ub: <http://example.edu/univ#>
SELECT ?name WHERE {
?s a ub:Student ; ub:name ?name ; ub:age ?age .
FILTER (?age > "21"^^xsd:integer)
}-
tools/shell/main.cppbuilds aDatabaseand aShellon stdin. -
tools/shell/shell.cppcounts braces, so a query may span lines.#inside<...>is not a comment. A backslash line is looked up in the command table intools/shell/commands.cpp; anything else is SPARQL. The line is handed to the session worker intools/shell/session.cpp. -
src/common/database.cppDatabase::Executecopies the currentConfigand calls the parser. -
src/parser/lexer.cppandsrc/parser/parser.cppproduce an AST.aisrdf:type.;and,repeat the subject and predicate.OPTIONAL,UNION, property paths, aggregates, andDELETE/INSERT WHEREthrowParseExceptionwith a line and column. -
src/binder/binder.cppresolves prefixes, looks constants up in the dictionary (interning them only forINSERT DATA), and assigns each variable a slot in first-seen order.src/common/term_codec.cppis the one spelling both the loader and the binder use (<iri>,"lex","lex"@en,"lex"^^<datatype>). -
src/planner/planner.cppbuilds a left-deep tree in textual order, whichsrc/optimizer/optimizer.cppthen reorders greedily fromStatistics(plan 5.3): oneTripleScanPlanper pattern,NestedLoopJoinPlanbetween them, thenFilterPlan, thenProjectionPlan.DISTINCTandLIMIT/OFFSETrun assrc/execution/executors/distinct_limit_executor.cpp(plan 4.4);ORDER BYruns assrc/execution/executors/sort_executor.cpp(plan 4.5): in memory when its input fits in half the pool, otherwise an external merge sort through pool pages. -
src/execution/execution_engine.cppaskssrc/execution/executor_factory.cppfor a volcano tree.Init/Nextpull rows ofterm_id_t. The scan issrc/execution/executors/triple_scan_executor.cpp. With\set join inljorhash,Optimizer::ApplyJoinMethodswaps each nested-loop node forIndexNestedLoopJoinPlanorHashJoinPlan(plans 4.2 and 4.3), and\explainprints the swapped tree. The nested-loop join issrc/execution/executors/nested_loop_join_executor.cpp; it re-opens the inner scan withInitWith(outer row), so the store sees the outer bindings as constants (plan 4.1). The filter issrc/execution/executors/filter_executor.cpp, usingsrc/execution/term_compare.cpp. The projection issrc/execution/executors/projection_executor.cpp. - The scan calls
src/store/mem_store.cpp, which returns asrc/store/mem_triple_iterator.cppover the triples whose bound positions match. Rows are decoded through the dictionary only when the engine builds the result. - The shell prints the header and the rows.
\explainprints the plan text from the same nodes and does not execute.
\set backend indexed (plan 3.4) builds a buffer pool, the catalog, a DiskDictionary, and an IndexedStore in a temporary file, moves the triples across by spelling, and runs statements one at a time; \set backend mem moves them back. The differential test in test/queries/differential_test.cpp runs every query file on both stores.
INSERT DATA runs as ModifyExecutor (plan 4.6) on whichever store is active. On the indexed backend it follows the path below. Steps 8 and 9 run when the transaction manager has a log, as in the crash harness; the shell's indexed backend has none. Each name is the file that owns that step.
- The shell and
Database::Executeparse and bind as above.src/planner/planner.cppemits anInsertPlan(src/include/execution/plans/stub_plans.h). -
src/execution/executors/modify_executor.cpp(plan 4.6) logs each triple that will change the store (TransactionManager::LogUpdate, plan 8.4), then writes it to the store, which writes every index the catalog knows or none; if a later triple fails, the statement's earlier ones are taken back. The binder has already written the dictionary. -
src/dictionary/disk_dictionary.cpp(plan 3.2, done; a term heap plus two B+ trees keyed by id and by hash) turns the three spellings into ids. Bytes live insrc/storage/term/term_heap.cpponsrc/include/storage/page/term_heap_page.h(plan 3.1, done; the page layout is in that header). -
src/storage/index/triple_key.cpp(plan 0.2, done) packs the three ids into 24 big-endian bytes.memcmpis the order. -
src/store/indexed_store.cpp(plan 3.4, done) inserts that key into each permutationsrc/catalog/catalog.cpp(plan 3.3, done; page 0, naming each tree's header page) has a root for. The tree issrc/storage/index/b_plus_tree.cpp(plans 2.2 to 2.7, done; latch crabbing makes it safe for concurrent use, plan 7.1). Leaves and internal pages aresrc/storage/page/b_plus_tree_leaf_page.handb_plus_tree_internal_page.h(plan 2.1, done; the byte layout is in those headers). The iterator issrc/storage/index/index_iterator.cpp; it copies a leaf's remaining keys, holds no latch or pin between rows, and stops atPrefixUpper. - The tree pins pages with
src/storage/page/page_guard.cpp(plan 1.5, done). Frames live insrc/buffer/buffer_pool_manager.cpp(plan 1.4, done; one latch, I/O through the scheduler, no flush in the destructor). Eviction issrc/buffer/lru_k_replacer.cpp(plan 1.3, done). - A dirty frame is written by
src/storage/disk/disk_scheduler.cpp(plan 1.2, done; one thread, requests in order, errors through the promise) ontosrc/storage/disk/disk_manager.cpp(plan 1.1, done; the file length records allocation, and freed ids are not reused). Every page image starts with the pageLSN insrc/storage/page/page.cpp. - When a dirty write guard is dropped, still latched, the pool appends a page record holding the page's image and sets pageLSN to it (
BufferPoolManager::LogPageWrite, plan 8.3). Before the page is written, the buffer pool flushes the log up to that pageLSN. A tree Insert or Remove opens aPageGroup(src/include/buffer/buffer_pool_manager.h): the log flushes nothing from the group's first LSN on until it closes, and the pool picks another victim than a page the group holds back, so a split or merge is never half on disk. The record is asrc/include/recovery/log_record.h(plan 8.1) appended bysrc/include/recovery/log_manager.h(plan 8.2). Commit forces the log. The commit record is the acknowledgement the crash harness trusts. - On the next open,
src/include/recovery/log_recovery.h(plan 8.6) runs analysis, redo, and undo. Redo copies a page image when the page's pageLSN is older. Undo of a triple is logical and writes a CLR (plan 8.4). A split or merge is redone, not logically undone.CheckpointManager(plan 8.5) is sharp: it flushes every page, then writes the record.
\crash exits without flushing. ontodb <dir> opens the durable memory database (snapshot plus logical WAL, below), not the page store.
-
src/include/storage/page/page.his a real frame type, not in the original file list. pageLSN has to live in one place so the buffer pool, the tree, and the log share it. -
src/include/common/database.his the facade the shell and the harnesses call. -
src/include/common/crash_point.his the named hook multi-page operations call. Tests arm those names. -
src/include/common/term_codec.his the shared spelling of IRIs, literals, and blanks. -
IndexedStore::InsertandDeletevisit every index rather than trusting the first. Undo after a crash can find one triple in some trees and not others; repeating the operation on every tree repairs them. -
LogRecordType::kPageis not in the original record list. Redo is physical: a page record carries the page's bytes after the pageLSN, so redo needs no knowledge of B+ tree layout. -
src/include/common/session_state.hcarries the lock-wait reason so the lock manager does not depend on the shell. -
src/include/execution/plans/abstract_plan.h,abstract_executor.h, andstub_plans.hare the volcano bases. Stub plans exist so\explaincan printSort,Distinct,Limit,Insert, andDeletebefore those executors exist. -
Schemalives insrc/include/common/types.h.lsn_tisint64_tso a long benchmark does not wrap the page header. -
MemStoreandMemDictionarytake a mutex so two shell sessions can share them. That mutex is not isolation. -
DiskManager,DiskScheduler, andBufferPoolManagertake optionalIoStats*andTraceSink*. - B+ tree page classes wrap a
Page*. The byte layout (plan 2.1) is documented in their headers. -
TransactionManagertakes an optionalLogManager*. Null means write-set undo (plan 7.2). Non-null means log undo (plan 8.4). -
\trace on|offswitchesSwitchableTraceSink, which prints component events to stderr.\bpmand\pagereport that the memory backend has no buffer pool. - The devcontainer installs
default-jre-headlesssoscripts/fetch-data lubmcan run the official generator. - Targets compile with
-Wno-unused-private-fieldbecause stub objects store the pointers the student will use. -
Pagecarries a reader-writer latch (RLatch/WLatch) for the guards, so aPageis neither copyable nor movable.Resetgives the frame a fresh latch when it is about to hold another page, so a latch belongs to one page. -
.clang-tidyenablesbugprone-*andclang-analyzer-*. The broadermodernizeandreadabilitysets are left off so the tidy job can stay green on a scaffold of intentional stubs. -
data/pizza.ttlis a small original subset used by the query tests.scripts/fetch-data pizzadownloads the real ontology intodata/downloads/, which is gitignored.
This path runs today when the shell was started with a directory (ontodb <dir>).
-
Database::Open(src/common/database.cpp) builds the database and aDurability(src/recovery/durability.cpp) over the same dictionary and store.Durability::Openloadssnapshot(src/recovery/snapshot.cpp), scanswal(ScanLoginsrc/recovery/log_manager.cpp), lets the recovery mode's damage policy decide about a bad tail, and replays the committed transactions. -
Database::Executeparses and binds as above. An update runs insideDurability::Run, against aRecordingStore(src/include/store/recording_store.h) that remembers each triple it changed. -
Durability::LogTransactionappends begin, any new dictionary terms, one update per change, and commit throughLogManager, then forces the commit. If anything throws,RecordingStore::Undotakes the changes back. -
\checkpointwrites a new snapshot by rename and empties the log.\verifyruns four checks (snapshot, log, replay, invariants), one report line each.
The code uses a small set of standard patterns, so a new method plugs in at a known place.
| Pattern | Where |
|---|---|
| Iterator (volcano) |
TripleIterator; every executor's Init / Next pulls one row at a time |
| Repository |
TripleStore and Dictionary hide where triples and terms live (MemStore, IndexedStore; MemDictionary, DiskDictionary) |
| Factory |
ExecutorFactory::Create maps a plan node to its executor; Database::Open; LogRecord::Begin, Commit, Update, and the other named constructors; MakeDamagePolicy
|
| Builder |
Planner builds the plan tree from a bound query |
| Strategy, replace conditional with polymorphism |
LogDamagePolicy with one class per recovery mode; \set join selects the join operator; TraceSink implementations |
| Null object |
NullTraceSink when tracing is off |
| Decorator |
RecordingStore wraps any TripleStore and records its changes |
| Adapter |
RdfLoader adapts raptor2's parser to Dictionary and TripleStore; an exception from either is caught in the C callback, the parse is aborted, and it is rethrown after raptor is freed |
| Command | The shell's command table (tools/shell/commands.cpp): one entry per backslash command, read by both dispatch and \help; log records are replayable commands |
| RAII |
ReadPageGuard and WritePageGuard unpin on destruction |
| Dependency injection |
ExecutorContext carries the store, dictionary, config, and stats into every executor; LogManager, Durability, and the buffer pool take their collaborators as constructor arguments |
Deliberate exceptions:
- No retry on a failed fsync. A log flush that fails is reported, not retried. After a failed fsync the kernel may have dropped the dirty pages, so a retry that succeeds can still have lost data; this is the lesson of PostgreSQL's 2018 fsync incident.
-
One process-wide test hook.
CrashPointis global because a crash point has to be reachable from any depth of any call. It does nothing unless a test arms it. -
Constants, not literals. Sizes and intervals have names:
PAGE_SIZEand the defaults insrc/include/common/config.h,LogRecord::kHeaderSizeandkMaxSize,kFlushIntervalin the log manager, andkDefaultLogTailin the shell.
Take this, after \load data/downloads/go/go.nt (see scripts/fetch-data go), \hindex build, \vload data/downloads/go/go.ovec, and \vindex build hnsw:
\similar "apoptotic process" 5 under "cell death"
-
tools/shell/commands.cpphands the line toDatabase::Semantic(), aSemanticEnginemade on first use (src/semantic/semantic_engine.cpp). It owns a private buffer pool on a temporary file, so the label tree and the IVF lists work on either backend. A backend switch drops it, because it names terms by the dictionary's ids. -
SemanticEngine::Similarsplits the line into units (<iri>,"text", bare words) and resolves each to a term id: an IRI through the dictionary, anything else through therdfs:labeltriples (an OSP scan for the literal). - The term's own vector is the query.
src/semantic/vector_store.cppholds one unit float vector per term id, loaded from a checksummed file whose records name terms by spelling. -
src/semantic/hybrid_search.cpppicks a strategy. Withunder,kAutoresolves the class when it has at most 4,096 members:LabelIndex::Scan(src/semantic/label_index.cpp) does one B+ tree range scan per interval of the class (BPlusTree::BeginRange), the intervals coming fromClassHierarchy(src/semantic/class_hierarchy.cpp, postorder labels and interval sets, plan 14.1). Above 4,096 it searches HNSW with a predicate instead: a row is allowed when its label lies in one of the class's intervals. - The scoring is
FlatIndex::SearchRows(exact,src/semantic/flat_index.cpp) orHnswIndex::Search(src/semantic/hnsw_index.cpp).IvfIndex(src/semantic/ivf_index.cpp) is the third choice: its lists are ranges of a B+ tree keyed(list, row, 0). - The engine prints each neighbor with its score, IRI, and label, and names the strategy and the time.
The theory is in the Semantic-Search page; every number is in benchmarks/semantic/results.md.