Namespace allocation and fallback modes
Fluree encodes IRIs as compact SIDs: a (ns_code, local) pair where:
ns_codeis au16namespace code that identifies an IRI prefixlocalis the remaining suffix (bytes) after removing the matched prefix
The database maintains a namespace table (LedgerSnapshot.namespace_codes: ns_code -> prefix string).
That table is embedded in the published index root and is loaded whenever a LedgerSnapshot is opened.
This document describes how Fluree chooses a namespace prefix for an IRI, and how it mitigates datasets that would otherwise allocate an excessive number of distinct namespace prefixes.
Goals
- Keep declared namespaces intact: if a dataset declares
@prefix foo: <...>, we want IRIs in that namespace to use that exact prefix, not a derived/split prefix. - Stable behavior across writes: after importing an “outlier” dataset, subsequent transactions should continue using the same fallback rules for previously unseen IRIs (e.g. new hosts), avoiding regression back to finer-grained splitting.
- Contain namespace explosion: avoid allocating one namespace code per highly-specific leaf
(e.g. splitting on the last
/for IRIs whose paths are effectively unique).
Core rule: declared-prefix trie match wins
Namespace resolution is trie-first:
- Load all known prefixes (predefined defaults + DB namespace table) into a byte-level trie.
- For each IRI, perform a longest-prefix match.
- If a match is found, emit
Sid(ns_code, iri[prefix_len..])and do not run fallback logic.
Only IRIs with no matching prefix fall through to the fallback splitter.
Implementation: fluree-db-transact/src/namespace.rs
NamespaceRegistry::sid_for_iri(transactions, serial paths)SharedNamespaceAllocator::sid_for_iri(parallel bulk import)
Fallback split modes (only for unmatched IRIs)
For IRIs that match no known prefix, the split is derived by canonical_split(iri, mode), where
mode is an NsSplitMode — a ledger-level property, not a per-call flag.
#![allow(unused)]
fn main() {
pub enum NsSplitMode {
#[default]
MostGranular, // finest-grained; the default for new ledgers
HostPlusN(u8), // scheme://host/ plus n additional path segments
}
}
MostGranular(default): split at the last/or#for hierarchical IRIs, or the last/ | # | :for opaque IRIs.HostPlusN(n): split atscheme://host/plus up to n additional non-empty path segments. For opaque IRIs, split atscheme:plus the first segment plus up to n further colon-delimited segments.HostPlusN(0)≈ host-only splittingHostPlusN(1)≈ host + one path segment
The mode persists as a single byte on the ledger: 0x00 = MostGranular,
0x01..=0xFF = HostPlusN(n-1). HOST_PLUS_N_MAX = 254, because HostPlusN(255) would wrap to
MostGranular on decode.
Implementation: fluree-db-core/src/ns_encoding.rs (NsSplitMode, canonical_split).
The registry reads it via LedgerSnapshot::ns_split_mode() and applies it in
fluree-db-transact/src/namespace.rs.
Bulk import: streaming preflight + dynamic mitigation
For large Turtle streaming imports, Fluree attempts to detect “namespace explosion” early without an extra I/O pass:
StreamingTurtleReadersamples bounded byte windows (NS_PREFLIGHT_WINDOW_SIZE, 8 MiB) within the first chunk region and counts distinct prefixes under the defaultMostGranularsplit.- If the sample exceeds
NS_PREFLIGHT_BUDGET(currently 255), the reader publishes aNamespaceSuggestion::CoarseHeuristicpreflight result recommending mitigation. - The import forwarder sets
NsSplitMode::HostPlusN(1)on the shared allocator before parsing begins, so the earliest allocations are already coarse.
Implementation:
- Preflight detector:
fluree-graph-turtle/src/splitter.rs - Policy application:
fluree-db-api/src/import.rs - Shared allocator:
SharedNamespaceAllocator::set_split_modeinfluree-db-transact/src/namespace.rs
Note:
NamespaceSuggestion::CoarseHeuristicis an import-side signal, not a split mode. It is the preflight’s recommendation; the mode it selects isHostPlusN(1).
The mode is a ledger property, not a per-open derivation
The split mode chosen at import time persists as part of the ledger, so subsequent normal transactions keep splitting unmatched IRIs the same way. There is no re-derivation from the size of the namespace table and no runtime escalation between modes.
LedgerSnapshot::ns_split_mode()returns the persisted mode.NamespaceRegistry::from_db(snapshot)seeds the registry with it.LedgerSnapshot::set_ns_split_mode(mode, commit_t)is immutable after user namespace allocation: if user namespaces already exist under a different mode, it returns an error (ns_split_mode conflict: commit t=… declares … but ledger already has user namespaces under …) rather than silently re-splitting.
That immutability is the point — a ledger whose IRIs were encoded under one split cannot have later IRIs encoded under another, or the same logical IRI would resolve to two different SIDs.
So for a ledger imported under HostPlusN(0), a later unseen IRI:
http://some-unseen-host/blah/123/456
allocates (if needed) at:
http://some-unseen-host/
Implementation: NamespaceRegistry::from_db and NamespaceRegistry::sid_for_iri in
fluree-db-transact/src/namespace.rs; set_ns_split_mode in fluree-db-core/src/db.rs
Notes and trade-offs
HostPlusN(0)can still result in many namespaces if a dataset genuinely contains many distinct hosts (one per host), but it prevents deeper fragmentation that is common in path-heavy IRIs.- The
OVERFLOWnamespace code is a sentinel used whenu16codes are exhausted; it is not a fallback mode. Overflow SIDs store the full IRI as the SID name. - High-cardinality trailing segments allocate one namespace per subject. Under
MostGranular, an opaque IRI splits at its last/,#or:, so a shape likeurn:example:record:<id>:rev:<hash>yields the prefixurn:example:record:<id>:rev:— unique to that one subject. A dataset built this way allocates a namespace code per node rather than per family, which has two consequences: the user code space (USER_START..OVERFLOW, ~65.5k codes) can be exhausted by a few hundred thousand subjects, and per-transaction work that keys off namespace codes degrades. The streaming preflight above only covers bulk import — ordinary transactions get no such detection, so this is a data-modelling concern for write workloads. The remedy is to move the high-cardinality part into the suffix (urn:example:record:<id>-rev-<hash>), so the whole family shares one prefix. There is no user-facing knob for the split mode —HostPlusN(n)is selected only by the bulk-import preflight above (fluree-db-api/src/import.rs), and the mode is immutable once the ledger’s genesis commit is written, so it cannot be changed on an existing ledger.