Skip to main content

SecondaryDatabase

Struct SecondaryDatabase 

Source
pub struct SecondaryDatabase { /* private fields */ }
Expand description

A secondary (index) database handle.

Secondary databases are always associated with a primary database. Key characteristics:

  • Direct put calls are prohibited; use the primary database instead.
  • delete on a secondary deletes the primary record (and all its secondary index entries).
  • get returns primary record data, not secondary data.
  • open_cursor returns a SecondaryCursor.

§v1.5 limitations

  • One-to-one only (Decision 1B): a given secondary key may map to at most one primary record. Sorted-dup secondaries are planned for v1.6. Two distinct primaries that produce the same secondary key cause the second update_secondary to fail with NoxuError::Unsupported.
  • Foreign-key constraints not enforced (Decision 2C): SecondaryDatabase::open rejects SecondaryConfigs whose foreign-key fields are set. Full FK support is planned for v1.6.
  • No automatic maintenance: callers manually invoke update_secondary after each primary put / delete. An automatic associate()-style hook is planned for v1.6.

§Atomicity with the primary write

update_secondary participates in the caller’s transaction when one is supplied. Threading the same txn through both Database::put and update_secondary makes the primary + secondary update atomic: aborting the txn rolls both back, committing the txn persists both. Passing None runs each call auto-committed, which restores the v1.4 behaviour and is acceptable when the caller does not need cross-database atomicity.

See docs/src/internal/v1.5-decisions-2026-05.md and docs/src/transactions/secondary-with-txn.md.

§Example

use noxu_db::{Database, DatabaseEntry};
use noxu_db::secondary_config::{SecondaryConfig, SecondaryKeyCreator};
use noxu_db::secondary_database::SecondaryDatabase;

struct MyKeyCreator;
impl SecondaryKeyCreator for MyKeyCreator { /* ... */ }

let sec_config = SecondaryConfig::new()
    .with_allow_create(true)
    .with_allow_populate(true)
    .with_key_creator(Box::new(MyKeyCreator));

let secondary = SecondaryDatabase::open(primary_db, "my_index", sec_config)?;

Implementations§

Source§

impl SecondaryDatabase

Source

pub fn open( primary: Arc<Mutex<Database>>, secondary_db: Database, config: SecondaryConfig, ) -> Result<Self>

Opens or creates a secondary database associated with primary.

§Arguments
  • primary - The primary database handle, shared via Arc<Mutex<_>>.
  • secondary_db - An already-opened Database that will serve as the underlying storage for the secondary index.
  • config - The secondary configuration (must include a key creator).
§Errors
  • NoxuError::IllegalArgument if the configuration is invalid, or if the inner secondary_db was not opened with DatabaseConfig::with_sorted_duplicates(true) (v1.6 sorted-dup secondaries — closes audit C4).
  • NoxuError::Unsupported if the configuration sets any foreign-key constraint field (foreign_key_database, foreign_key_delete_action != Abort, foreign_key_nullifier, or foreign_multi_key_nullifier). v1.5 does not enforce FK constraints; full FK support is planned for v1.6 — see Decision 2C in docs/src/internal/v1.5-decisions-2026-05.md (closes audit findings C2 / F1 / F16).
Source

pub fn get_database_name(&self) -> &str

Returns the database name of the secondary index.

Source

pub fn get_config(&self) -> &SecondaryConfig

Returns the secondary configuration.

Source

pub fn is_valid(&self) -> bool

Returns whether this handle is open.

Source

pub fn close(&self) -> Result<()>

Closes the secondary database handle.

Source

pub fn count(&self) -> Result<u64>

Returns the number of records in the secondary index.

Equivalent to Database::count on the underlying inner index database; included on SecondaryDatabase for symmetry with JE’s SecondaryDatabase.count() method. See (secondary-join “missing count/exists/truncate” Low).

§Errors

Returns NoxuError::DatabaseClosed if the secondary handle has been closed.

Source

pub fn exists( &self, txn: Option<&Transaction>, key: &DatabaseEntry, ) -> Result<bool>

Returns true if any record with the given secondary key exists.

This avoids the cost of reading the primary record — unlike Self::get, which traverses the secondary, then the primary database. Useful for membership probes inside hot paths.

§Errors

Propagates any error from the underlying secondary lookup.

Source

pub fn truncate(&self) -> Result<u64>

Removes every record from the secondary index, leaving the associated primary database untouched.

Caveat. Truncating a secondary index without re-running populate_if_empty (or replaying the primary-side updates) leaves the secondary in a state that is not consistent with the primary. Most callers should drop the secondary’s primary keys via Database::truncate_database on the inner DB or repopulate the index afterwards. Returned for symmetry with JE’s SecondaryDatabase.truncate(...).

Returns the number of records that were in the index before the truncate.

§Errors

Returns NoxuError::DatabaseClosed if the secondary handle has been closed, or any error returned by the underlying delete loop.

Source

pub fn get( &self, txn: Option<&Transaction>, key: &DatabaseEntry, p_key: &mut DatabaseEntry, data: &mut DatabaseEntry, ) -> Result<OperationStatus>

Retrieves a primary record by secondary key.

Looks up key in the secondary index, obtains the primary key stored there, then fetches the corresponding record from the primary database.

§Arguments
  • txn - Optional transaction.
  • key - The secondary key to search for.
  • p_key - Output: receives the primary key found.
  • data - Output: receives the primary record data.
§Returns

OperationStatus::Success if found; OperationStatus::NotFound otherwise.

Source

pub fn delete( &self, txn: Option<&Transaction>, key: &DatabaseEntry, ) -> Result<OperationStatus>

Deletes all primary records whose secondary key equals key.

All duplicate secondary index entries with the given secondary key are found and their corresponding primary records deleted. Each primary deletion in turn removes all secondary index entries for that primary record.

§Arguments
  • txn - Optional transaction.
  • key - The secondary key whose primary records should be deleted.
§Returns

OperationStatus::Success if at least one record was deleted; OperationStatus::NotFound if the key was not found.

Source

pub fn open_cursor<'a>( &'a self, txn: Option<&'a Transaction>, config: Option<&CursorConfig>, ) -> Result<SecondaryCursor<'a>>

Opens a cursor on the secondary database.

When txn is Some(_), the inner cursor over the secondary index participates in the supplied transaction — reads acquire shared locks via the txn’s locker and writes acquire exclusive locks tracked by the txn. Secondary cursors also this to the primary lookups and the SecondaryCursor::delete cascade as well: the cursor stores the txn handle and forwards it to every primary get / delete and to delete_all_for_primary. Aborting the txn rolls back both the secondary entry and the primary record removed by SecondaryCursor::delete (and every secondary cleanup it triggers). When txn is None, every operation runs auto-committed, matching the v1.4 behaviour.

config is forwarded to the inner Database::open_cursor call so read_uncommitted and other cursor-level flags propagate correctly.

§Lifetime contract

The returned SecondaryCursor borrows both the SecondaryDatabase and — when supplied — the Transaction, because primary deletes and cleanup writes are deferred until SecondaryCursor::delete is called. Callers must therefore keep the Transaction alive at least as long as the cursor. In practice this is the same lifetime rule that already applies to Database::open_cursor; it is now enforced statically by the type system.

§Returns

A SecondaryCursor that iterates secondary index entries and returns primary data.

Source

pub fn start_incremental_population(&self)

Starts incremental population mode.

Source

pub fn end_incremental_population(&self)

Ends incremental population mode.

Source

pub fn is_incremental_population_enabled(&self) -> bool

Returns whether incremental population is currently enabled.

Source

pub fn update_secondary( &self, txn: Option<&Transaction>, pri_key: &DatabaseEntry, old_data: Option<&DatabaseEntry>, new_data: Option<&DatabaseEntry>, ) -> Result<()>

Updates the secondary index when a primary record is inserted or updated.

Called from application code that manages secondary index updates manually (v1.5 has no automatic associate()-style hook — that is v1.6 work).

§Atomicity

When txn is Some(&t), all I/O performed by this method (cursor opens, insert_sec_key, delete_sec_key) is executed under t. If the caller used the same t for the primary Database::put / Database::delete that prompted this update, the primary write and every affected secondary index entry commit or abort together. This is the recommended pattern; see docs/src/transactions/secondary-with-txn.md.

When txn is None, every inner secondary write runs auto-committed (v1.4 behaviour). This is intentionally available so callers that do not need cross-database atomicity — e.g. one-shot population or single-threaded scripts — do not need to allocate a transaction.

Idempotent re-insert (Decision 1B): if update_secondary is invoked twice with the same (sec_key, pri_key) pair (whether auto-commit or under the same txn), the second call is a no-op rather than a NoxuError::Unsupported collision — see Self::insert_sec_key.

§Arguments
  • txn - Optional transaction. Pass the same handle that drives the primary write to make both updates atomic.
  • pri_key - The primary key.
  • old_data - The previous primary data, or None on insert.
  • new_data - The new primary data, or None on delete.

Trait Implementations§

Source§

impl Drop for SecondaryDatabase

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more