Skip to main content

noxu_db/
secondary_database.rs

1//! Secondary database handle.
2//!
3//!
4//! A secondary database is an index over a primary database.  Records are
5//! automatically maintained when the primary is written.  Reads via a
6//! secondary return primary data; deletes via a secondary delete the
7//! corresponding primary record.
8//!
9//! The mapping of secondary keys to primary records is stored in an ordinary
10//! Database whose records have the form:
11//!
12//!   key   = secondary_key
13//!   value = primary_key
14//!
15//! On every primary `put` the secondary is updated via `update_secondary`.
16//! On every primary `delete` the secondary entry is removed.
17//!
18//! # Atomicity with the primary write (Sprint 4½)
19//!
20//! [`SecondaryDatabase::update_secondary`] takes an explicit
21//! `Option<&Transaction>` parameter that is forwarded to every
22//! [`Database`] operation it performs against the inner secondary index.
23//! When the caller threads the *same* `txn` through
24//! [`Database::put`] / [`Database::delete`] **and**
25//! [`SecondaryDatabase::update_secondary`], the primary write and the
26//! secondary index update are atomic — committing or aborting the txn
27//! commits or rolls back **both** sides together.  See
28//! `docs/src/transactions/secondary-with-txn.md` for the canonical
29//! pattern.
30//!
31//! Pre-Sprint-4½ (v1.4 / v1.5.0-rc1 / v1.5.0-rc2) `update_secondary` ran
32//! auto-committed regardless of any caller transaction, leaving a
33//! partial-atomicity gap (see audit Theme 2 / finding F5): an aborted
34//! primary `put` could leave the secondary entry behind on disk.  The
35//! gap is closed for the manual-update pattern.  Automatic
36//! `associate()`-style maintenance — where `Database::put` itself
37//! drives all attached secondaries inside the same txn — remains v1.6
38//! work.
39//!
40//! # v1.5 limitations
41//!
42//! See `docs/src/internal/v1.5-decisions-2026-05.md`.
43//!
44//! - **Decision 1B** — v1.5 secondaries are honestly **one-to-one**: a given
45//!   secondary key may map to at most one primary key.  Two distinct
46//!   primaries that produce the same secondary key cause the second
47//!   `update_secondary` (or `populate_if_empty`) to fail with a typed
48//!   [`NoxuError::Unsupported`] (closes audit finding C4).  Sorted-dup
49//!   secondaries are planned for v1.6.
50//! - **Decision 2C** — foreign-key constraints are not enforced in v1.5.
51//!   [`SecondaryDatabase::open`] rejects any [`SecondaryConfig`] whose
52//!   foreign-key fields are set with [`NoxuError::Unsupported`] (closes
53//!   audit findings C2, F1, F16).  Full FK support is planned for v1.6.
54//! - **Automatic secondary maintenance** is not implemented in v1.5;
55//!   callers must invoke `update_secondary` manually after each primary
56//!   `put` / `delete` (planned for v1.6).
57
58use crate::cursor::Cursor;
59use crate::cursor_config::CursorConfig;
60use crate::database::Database;
61use crate::database_entry::DatabaseEntry;
62use crate::error::{NoxuError, Result};
63use crate::operation_status::OperationStatus;
64use crate::secondary_config::SecondaryConfig;
65use crate::secondary_cursor::SecondaryCursor;
66use crate::transaction::Transaction;
67use noxu_dbi::{CursorImpl, GetMode};
68use noxu_sync::Mutex;
69use std::cell::RefCell;
70use std::collections::HashSet;
71use std::sync::Arc;
72use std::sync::atomic::{AtomicBool, Ordering};
73
74thread_local! {
75    /// Cycle-detection frame for FK cascades and nullifications.
76    /// Contains every `(db_id, fk_value)` pair the current thread is
77    /// in the middle of cascading.  See [`FkReferrer`].
78    static FK_CASCADE_GUARD: RefCell<HashSet<(u64, Vec<u8>)>> =
79        RefCell::new(HashSet::new());
80}
81
82/// Trait implemented by [`SecondaryHookState`] so a primary
83/// [`Database`] can keep the secondary registry as
84/// `Vec<Weak<dyn SecondaryHook + Send + Sync>>` without naming the
85/// concrete state struct (the struct holds a non-`Send` config field
86/// for some user-supplied callbacks; the trait only exposes the
87/// txn-driven update entry point and the secondary's name for
88/// diagnostics).
89///
90/// v1.6 (audit C3 — the `associate()`-style hook).
91pub(crate) trait SecondaryHook {
92    /// Updates this secondary index after a primary write.  Called by
93    /// `Database::put` (`old_data` = `None`, `new_data = Some(…)`),
94    /// `Database::delete` (`old_data = Some(…)`, `new_data = None`),
95    /// or a primary update path (both `Some`).  `txn` is the same
96    /// transaction that drove the primary write so the secondary update
97    /// participates atomically.
98    fn maintain(
99        &self,
100        txn: Option<&Transaction>,
101        pri_key: &DatabaseEntry,
102        old_data: Option<&DatabaseEntry>,
103        new_data: Option<&DatabaseEntry>,
104    ) -> Result<()>;
105
106    /// Returns the secondary's database name (used in diagnostics).
107    fn name(&self) -> String;
108}
109
110/// Trait implemented by [`SecondaryHookState`] for the FK referrer
111/// registry (v1.6 audit C2 / Decision 2C).  When a record is deleted
112/// from a foreign-key target primary, the engine iterates every
113/// registered referrer and calls
114/// [`FkReferrer::on_foreign_key_deleted`] under the same caller-supplied
115/// txn.  The implementation runs the configured
116/// [`ForeignKeyDeleteAction`] for every secondary key matching the
117/// deleted foreign key.
118pub(crate) trait FkReferrer {
119    /// Called when a foreign-DB primary record is about to be deleted.
120    /// `fk_value` is the primary key of the foreign DB record (which
121    /// may also be a secondary key in this child index).
122    ///
123    /// Returning `Err(NoxuError::ForeignConstraintViolation(…))` aborts
124    /// the foreign delete (Abort action).  Returning `Ok(())` may have
125    /// already mutated the child primary records (Cascade / Nullify).
126    fn on_foreign_key_deleted(
127        &self,
128        txn: Option<&Transaction>,
129        fk_value: &DatabaseEntry,
130    ) -> Result<()>;
131
132    /// Returns the child secondary's database name (used in error messages).
133    fn name(&self) -> String;
134}
135
136/// Internal state of a [`SecondaryDatabase`].
137///
138/// Held behind an `Arc` so the primary database can keep a `Weak<_>`
139/// reference for automatic-maintenance fan-out without creating a
140/// cycle.  Dropping the [`SecondaryDatabase`] handle drops the strong
141/// `Arc`; the next primary registration will purge the now-dangling
142/// `Weak`.
143pub(crate) struct SecondaryHookState {
144    /// The underlying secondary index storage (sec_key -> [pri_key…]).
145    pub(crate) inner: Database,
146    /// The primary database this index is associated with.
147    pub(crate) primary: Arc<Mutex<Database>>,
148    /// The secondary configuration (holds key creator callback, etc.).
149    pub(crate) config: SecondaryConfig,
150    /// Whether this secondary is fully populated (not in incremental mode).
151    pub(crate) is_fully_populated: AtomicBool,
152}
153
154impl SecondaryHookState {
155    /// Updates this secondary index after a primary insert / update /
156    /// delete.  Mirrors the v1.5 [`SecondaryDatabase::update_secondary`]
157    /// behaviour but lives on the state so it can be invoked from the
158    /// [`SecondaryHook`] trait impl as well as the public
159    /// [`SecondaryDatabase`] facade.
160    pub(crate) fn update_secondary(
161        &self,
162        txn: Option<&Transaction>,
163        pri_key: &DatabaseEntry,
164        old_data: Option<&DatabaseEntry>,
165        new_data: Option<&DatabaseEntry>,
166    ) -> Result<()> {
167        let key_creator = &self.config.key_creator;
168        let multi_key_creator = &self.config.multi_key_creator;
169
170        if old_data.is_none() && new_data.is_none() {
171            return Ok(());
172        }
173
174        if let Some(creator) = key_creator {
175            let old_sec_key = old_data.and_then(|od| {
176                let mut sk = DatabaseEntry::new();
177                if creator.create_secondary_key(
178                    &self.inner,
179                    pri_key,
180                    od,
181                    &mut sk,
182                ) {
183                    Some(sk)
184                } else {
185                    None
186                }
187            });
188            let new_sec_key = new_data.and_then(|nd| {
189                let mut sk = DatabaseEntry::new();
190                if creator.create_secondary_key(
191                    &self.inner,
192                    pri_key,
193                    nd,
194                    &mut sk,
195                ) {
196                    Some(sk)
197                } else {
198                    None
199                }
200            });
201            let do_delete = old_sec_key.is_some()
202                && old_sec_key.as_ref() != new_sec_key.as_ref();
203            let do_insert = new_sec_key.is_some()
204                && new_sec_key.as_ref() != old_sec_key.as_ref();
205            if do_delete {
206                self.delete_sec_key(
207                    txn,
208                    old_sec_key.as_ref().unwrap(),
209                    pri_key,
210                )?;
211            }
212            if do_insert {
213                self.insert_sec_key(
214                    txn,
215                    new_sec_key.as_ref().unwrap(),
216                    pri_key,
217                )?;
218            }
219        } else if let Some(multi_creator) = multi_key_creator {
220            let empty = Vec::<DatabaseEntry>::new();
221            let old_keys: Vec<DatabaseEntry> = if let Some(od) = old_data {
222                let mut keys = Vec::new();
223                multi_creator.create_secondary_keys(
224                    &self.inner,
225                    pri_key,
226                    od,
227                    &mut keys,
228                );
229                keys
230            } else {
231                empty.clone()
232            };
233            let new_keys: Vec<DatabaseEntry> = if let Some(nd) = new_data {
234                let mut keys = Vec::new();
235                multi_creator.create_secondary_keys(
236                    &self.inner,
237                    pri_key,
238                    nd,
239                    &mut keys,
240                );
241                keys
242            } else {
243                empty
244            };
245            for old_key in &old_keys {
246                if !new_keys.contains(old_key) {
247                    self.delete_sec_key(txn, old_key, pri_key)?;
248                }
249            }
250            for new_key in &new_keys {
251                if !old_keys.contains(new_key) {
252                    self.insert_sec_key(txn, new_key, pri_key)?;
253                }
254            }
255        }
256
257        Ok(())
258    }
259
260    /// Inserts a (sec_key, pri_key) duplicate.  See the
261    /// [`SecondaryDatabase`] impl for the full doc-comment; this is the
262    /// state-side implementation that the public method delegates to.
263    fn insert_sec_key(
264        &self,
265        txn: Option<&Transaction>,
266        sec_key: &DatabaseEntry,
267        pri_key: &DatabaseEntry,
268    ) -> Result<()> {
269        let mut cursor = self.make_inner_cursor(txn)?;
270        let status =
271            cursor
272                .put(sec_key, pri_key, crate::put::Put::NoDupData)
273                .map_err(|e| NoxuError::OperationNotAllowed(e.to_string()))?;
274        match status {
275            OperationStatus::Success => Ok(()),
276            OperationStatus::KeyExists => Ok(()),
277            other => Err(NoxuError::OperationNotAllowed(format!(
278                "unexpected put status from secondary index insert: {other:?}"
279            ))),
280        }
281    }
282
283    /// Deletes the exact (sec_key, pri_key) duplicate.
284    fn delete_sec_key(
285        &self,
286        txn: Option<&Transaction>,
287        sec_key: &DatabaseEntry,
288        pri_key: &DatabaseEntry,
289    ) -> Result<()> {
290        let mut cursor = self.make_inner_cursor(txn)?;
291        let mut sec_key_mut = sec_key.clone();
292        let mut pri_key_mut = pri_key.clone();
293        let status = cursor
294            .get(
295                &mut sec_key_mut,
296                &mut pri_key_mut,
297                crate::get::Get::SearchBoth,
298                None,
299            )
300            .map_err(|e| NoxuError::OperationNotAllowed(e.to_string()))?;
301        if status == OperationStatus::Success {
302            cursor
303                .delete()
304                .map_err(|e| NoxuError::OperationNotAllowed(e.to_string()))?;
305        }
306        Ok(())
307    }
308
309    fn make_inner_cursor(&self, txn: Option<&Transaction>) -> Result<Cursor> {
310        self.inner.open_cursor(txn, None)
311    }
312}
313
314impl SecondaryHook for SecondaryHookState {
315    fn maintain(
316        &self,
317        txn: Option<&Transaction>,
318        pri_key: &DatabaseEntry,
319        old_data: Option<&DatabaseEntry>,
320        new_data: Option<&DatabaseEntry>,
321    ) -> Result<()> {
322        self.update_secondary(txn, pri_key, old_data, new_data)
323    }
324
325    fn name(&self) -> String {
326        self.inner.get_database_name().to_string()
327    }
328}
329
330impl FkReferrer for SecondaryHookState {
331    /// v1.6 (audit C2 / Decision 2C): handles a foreign-DB delete by
332    /// dispatching on the configured [`ForeignKeyDeleteAction`].
333    ///
334    /// * `Abort`   — if any child secondary entry has `sec_key == fk_value`,
335    ///   return [`NoxuError::ForeignConstraintViolation`].
336    /// * `Cascade` — wired in step 9.
337    /// * `Nullify` — wired in step 10.
338    fn on_foreign_key_deleted(
339        &self,
340        txn: Option<&Transaction>,
341        fk_value: &DatabaseEntry,
342    ) -> Result<()> {
343        match self.config.foreign_key_delete_action {
344            crate::secondary_config::ForeignKeyDeleteAction::Abort => {
345                // Probe the inner secondary index for any duplicate of
346                // `fk_value`.  If we find at least one, the foreign
347                // delete must abort.
348                let mut probe_key = fk_value.clone();
349                let mut probe_pk = DatabaseEntry::new();
350                let mut cursor = self.inner.open_cursor(txn, None)?;
351                let st = cursor
352                    .get(
353                        &mut probe_key,
354                        &mut probe_pk,
355                        crate::get::Get::Search,
356                        None,
357                    )
358                    .map_err(|e| {
359                        NoxuError::OperationNotAllowed(e.to_string())
360                    })?;
361                if st == OperationStatus::Success {
362                    let fk_hex = fk_value
363                        .get_data()
364                        .map(|b| {
365                            b.iter()
366                                .map(|b| format!("{b:02x}"))
367                                .collect::<String>()
368                        })
369                        .unwrap_or_default();
370                    return Err(NoxuError::ForeignConstraintViolation(
371                        format!(
372                            "foreign-key delete aborted: secondary '{}' \
373                             still references foreign key 0x{fk_hex} \
374                             (ForeignKeyDeleteAction::Abort)",
375                            self.inner.get_database_name()
376                        ),
377                    ));
378                }
379                Ok(())
380            }
381            crate::secondary_config::ForeignKeyDeleteAction::Cascade => {
382                // v1.6 step 9 — transitive cascade with cycle detection.
383                //
384                // For every primary record indexed under `fk_value` in
385                // *this* secondary, delete the primary.  The primary's
386                // own [`Database::delete`] fan-out re-enters this hook
387                // for any deeper cascades; the thread-local guard keeps
388                // a cycle from spinning forever.
389                let primary = Arc::clone(&self.primary);
390                let db_id = primary.lock().db_id_for_fk_guard();
391                let fk_bytes = fk_value.get_data().unwrap_or(&[]).to_vec();
392
393                if !FK_CASCADE_GUARD
394                    .with(|c| c.borrow_mut().insert((db_id, fk_bytes.clone())))
395                {
396                    // Already cascading on this (db, key) frame — skip
397                    // to break the cycle.  This matches JE's
398                    // `cascadeDeletePrimaries` cycle-skip logic.
399                    return Ok(());
400                }
401
402                // Collect every child primary key indexed under fk_value.
403                let child_pris: Vec<DatabaseEntry> = {
404                    let mut child_keys = Vec::new();
405                    let mut cursor = self.inner.open_cursor(txn, None)?;
406                    let mut sk = fk_value.clone();
407                    let mut pk = DatabaseEntry::new();
408                    let mut st = cursor
409                        .get(&mut sk, &mut pk, crate::get::Get::Search, None)
410                        .map_err(|e| {
411                            NoxuError::OperationNotAllowed(e.to_string())
412                        })?;
413                    while st == OperationStatus::Success {
414                        if sk.get_data().unwrap_or(&[])
415                            != fk_value.get_data().unwrap_or(&[])
416                        {
417                            break;
418                        }
419                        if let Some(b) = pk.get_data() {
420                            child_keys.push(DatabaseEntry::from_bytes(b));
421                        }
422                        st = cursor
423                            .get(&mut sk, &mut pk, crate::get::Get::Next, None)
424                            .map_err(|e| {
425                                NoxuError::OperationNotAllowed(e.to_string())
426                            })?;
427                    }
428                    child_keys
429                };
430
431                // Apply the cascade.  Each `primary.delete` re-enters
432                // the maintenance plumbing on the child primary so its
433                // secondaries and any deeper FK relationships are
434                // honoured.  Errors propagate so the caller's txn rolls
435                // the cascade back together with the originating delete.
436                let cascade_result: Result<()> = (|| {
437                    let primary_guard = primary.lock();
438                    for child_pri in child_pris {
439                        primary_guard.delete(txn, &child_pri)?;
440                    }
441                    Ok(())
442                })();
443
444                FK_CASCADE_GUARD.with(|c| {
445                    c.borrow_mut().remove(&(db_id, fk_bytes));
446                });
447
448                cascade_result
449            }
450            crate::secondary_config::ForeignKeyDeleteAction::Nullify => {
451                // v1.6 step 10 — nullify the FK field on every child
452                // primary record indexed under fk_value, then re-put
453                // the modified record so auto-maintenance cleans up
454                // the now-stale secondary entry.
455                //
456                // Cycle detection mirrors the Cascade arm: even though
457                // a Nullify cannot directly cascade through more FK
458                // edges, a child primary update may itself be a
459                // foreign-key-delete from another perspective via the
460                // auto-maintenance fan-out, so we still guard the
461                // (db, key) frame.
462                let primary = Arc::clone(&self.primary);
463                let db_id = primary.lock().db_id_for_fk_guard();
464                let fk_bytes = fk_value.get_data().unwrap_or(&[]).to_vec();
465                if !FK_CASCADE_GUARD
466                    .with(|c| c.borrow_mut().insert((db_id, fk_bytes.clone())))
467                {
468                    return Ok(());
469                }
470
471                let single = self.config.foreign_key_nullifier.as_deref();
472                let multi = self.config.foreign_multi_key_nullifier.as_deref();
473
474                // Collect (child_primary_key, child_primary_data) pairs.
475                let child_records: Vec<(DatabaseEntry, DatabaseEntry)> = {
476                    let mut child = Vec::new();
477                    let mut cursor = self.inner.open_cursor(txn, None)?;
478                    let mut sk = fk_value.clone();
479                    let mut pk = DatabaseEntry::new();
480                    let mut st = cursor
481                        .get(&mut sk, &mut pk, crate::get::Get::Search, None)
482                        .map_err(|e| {
483                            NoxuError::OperationNotAllowed(e.to_string())
484                        })?;
485                    while st == OperationStatus::Success {
486                        if sk.get_data().unwrap_or(&[])
487                            != fk_value.get_data().unwrap_or(&[])
488                        {
489                            break;
490                        }
491                        // Fetch the child primary's data so the
492                        // nullifier sees it.
493                        let child_pri = DatabaseEntry::from_bytes(
494                            pk.get_data().unwrap_or(&[]),
495                        );
496                        let mut data = DatabaseEntry::new();
497                        let g =
498                            primary.lock().get(txn, &child_pri, &mut data)?;
499                        if g == OperationStatus::Success {
500                            child.push((child_pri, data));
501                        }
502                        st = cursor
503                            .get(&mut sk, &mut pk, crate::get::Get::Next, None)
504                            .map_err(|e| {
505                                NoxuError::OperationNotAllowed(e.to_string())
506                            })?;
507                    }
508                    child
509                };
510
511                let nullify_result: Result<()> = (|| {
512                    for (child_pri, mut child_data) in child_records {
513                        let modified = match (single, multi) {
514                            (Some(n), _) => n.nullify_foreign_key(
515                                &self.inner,
516                                &mut child_data,
517                            ),
518                            (None, Some(mn)) => mn.nullify_foreign_key(
519                                &self.inner,
520                                &child_pri,
521                                &mut child_data,
522                                fk_value,
523                            ),
524                            (None, None) => {
525                                return Err(NoxuError::IllegalArgument(
526                                    "ForeignKeyDeleteAction::Nullify requires a \
527                                     ForeignKeyNullifier or \
528                                     ForeignMultiKeyNullifier on the \
529                                     SecondaryConfig"
530                                        .to_string(),
531                                ));
532                            }
533                        };
534                        if modified {
535                            // Re-put the modified record under the
536                            // caller's txn.  Auto-maintenance on the
537                            // child primary handles clearing the stale
538                            // secondary entries.
539                            primary.lock().put(txn, &child_pri, &child_data)?;
540                        }
541                    }
542                    Ok(())
543                })();
544
545                FK_CASCADE_GUARD.with(|c| {
546                    c.borrow_mut().remove(&(db_id, fk_bytes));
547                });
548
549                nullify_result
550            }
551        }
552    }
553
554    fn name(&self) -> String {
555        self.inner.get_database_name().to_string()
556    }
557}
558
559/// A secondary (index) database handle.
560///
561///
562///
563/// Secondary databases are always associated with a primary database.
564/// Key characteristics:
565/// - Direct `put` calls are prohibited; use the primary database instead.
566/// - `delete` on a secondary deletes the primary record (and all its
567///   secondary index entries).
568/// - `get` returns primary record data, not secondary data.
569/// - `open_cursor` returns a [`SecondaryCursor`].
570///
571/// # v1.5 limitations
572///
573/// - **One-to-one only** (Decision 1B): a given secondary key may map to
574///   at most one primary record.  Sorted-dup secondaries are planned for
575///   v1.6.  Two distinct primaries that produce the same secondary key
576///   cause the second `update_secondary` to fail with
577///   [`NoxuError::Unsupported`].
578/// - **Foreign-key constraints not enforced** (Decision 2C):
579///   [`SecondaryDatabase::open`] rejects [`SecondaryConfig`]s whose
580///   foreign-key fields are set.  Full FK support is planned for v1.6.
581/// - **No automatic maintenance**: callers manually invoke
582///   [`update_secondary`](Self::update_secondary) after each primary
583///   `put` / `delete`.  An automatic `associate()`-style hook is planned
584///   for v1.6.
585///
586/// # Atomicity with the primary write
587///
588/// `update_secondary` participates in the
589/// caller's transaction when one is supplied.  Threading the same
590/// `txn` through both [`Database::put`] and
591/// [`update_secondary`](Self::update_secondary) makes the primary +
592/// secondary update **atomic**: aborting the txn rolls both back,
593/// committing the txn persists both.  Passing `None` runs each call
594/// auto-committed, which restores the v1.4 behaviour and is acceptable
595/// when the caller does not need cross-database atomicity.
596///
597/// See `docs/src/internal/v1.5-decisions-2026-05.md` and
598/// `docs/src/transactions/secondary-with-txn.md`.
599///
600/// # Example
601/// ```ignore
602/// use noxu_db::{Database, DatabaseEntry};
603/// use noxu_db::secondary_config::{SecondaryConfig, SecondaryKeyCreator};
604/// use noxu_db::secondary_database::SecondaryDatabase;
605///
606/// struct MyKeyCreator;
607/// impl SecondaryKeyCreator for MyKeyCreator { /* ... */ }
608///
609/// let sec_config = SecondaryConfig::new()
610///     .with_allow_create(true)
611///     .with_allow_populate(true)
612///     .with_key_creator(Box::new(MyKeyCreator));
613///
614/// let secondary = SecondaryDatabase::open(primary_db, "my_index", sec_config)?;
615/// ```
616pub struct SecondaryDatabase {
617    /// All shared state behind an `Arc` so the primary registry can
618    /// keep `Weak<dyn SecondaryHook + Send + Sync>` references
619    /// (Decision 1B / audit C3).  Every public method on
620    /// `SecondaryDatabase` accesses the fields through `state.field`.
621    state: Arc<SecondaryHookState>,
622}
623
624impl SecondaryDatabase {
625    /// Opens or creates a secondary database associated with `primary`.
626    ///
627    ///
628    ///
629    /// # Arguments
630    /// * `primary` - The primary database handle, shared via `Arc<Mutex<_>>`.
631    /// * `secondary_db` - An already-opened `Database` that will serve as the
632    ///   underlying storage for the secondary index.
633    /// * `config` - The secondary configuration (must include a key creator).
634    ///
635    /// # Errors
636    /// - [`NoxuError::IllegalArgument`] if the configuration is invalid,
637    ///   or if the inner `secondary_db` was not opened with
638    ///   `DatabaseConfig::with_sorted_duplicates(true)` (v1.6 sorted-dup
639    ///   secondaries — closes audit C4).
640    /// - [`NoxuError::Unsupported`] if the configuration sets any foreign-key
641    ///   constraint field (`foreign_key_database`,
642    ///   `foreign_key_delete_action != Abort`, `foreign_key_nullifier`, or
643    ///   `foreign_multi_key_nullifier`).  v1.5 does not enforce FK
644    ///   constraints; full FK support is planned for v1.6 — see Decision 2C
645    ///   in `docs/src/internal/v1.5-decisions-2026-05.md` (closes audit
646    ///   findings C2 / F1 / F16).
647    pub fn open(
648        primary: Arc<Mutex<Database>>,
649        secondary_db: Database,
650        config: SecondaryConfig,
651    ) -> Result<Self> {
652        // Validate the config w.r.t. the primary's read-only flag.
653        let primary_read_only = primary.lock().get_config().read_only;
654        config
655            .validate(primary_read_only)
656            .map_err(NoxuError::IllegalArgument)?;
657
658        // v1.6 (Decision 1B / audit C4): the inner secondary index DB
659        // must be opened with sorted_duplicates so multiple primary
660        // records can share the same secondary key as duplicates of
661        // the (sec_key) entry.  Reject otherwise — in v1.5 we used
662        // Put::NoOverwrite and surfaced cross-primary collisions as
663        // NoxuError::Unsupported; v1.6 stores them as duplicates.
664        if !secondary_db.get_config().sorted_duplicates {
665            return Err(NoxuError::IllegalArgument(
666                "v1.6 secondary databases require the inner index DB to \
667                 be opened with DatabaseConfig::with_sorted_duplicates(true) \
668                 — see docs/src/internal/v1.5-decisions-2026-05.md Decision 1B"
669                    .to_string(),
670            ));
671        }
672
673        // v1.6 (audit C2 / Decision 2C): foreign-key constraints are
674        // now enforced when the user supplies the foreign DB handle
675        // via [`SecondaryConfig::with_foreign_key_database_handle`].
676        // The `name`-only setter remains advisory — a config that
677        // names a foreign DB but never wires the handle is rejected
678        // here so the user is not silently left with an unenforced
679        // constraint.  Cascade / Nullify still require the handle and
680        // the matching nullifier (steps 9 / 10).
681        let fk_handle = config.foreign_key_database.clone();
682        if config.foreign_key_database_name.is_some() && fk_handle.is_none() {
683            return Err(NoxuError::IllegalArgument(
684                "SecondaryConfig.foreign_key_database_name is set without \
685                 a foreign_key_database handle; v1.6 FK enforcement requires \
686                 calling SecondaryConfig::with_foreign_key_database_handle()"
687                    .to_string(),
688            ));
689        }
690        if (config.foreign_key_nullifier.is_some()
691            || config.foreign_multi_key_nullifier.is_some())
692            && fk_handle.is_none()
693        {
694            return Err(NoxuError::IllegalArgument(
695                "foreign-key nullifier is set without a foreign_key_database \
696                 handle (call SecondaryConfig::with_foreign_key_database_handle)"
697                    .to_string(),
698            ));
699        }
700
701        let state = Arc::new(SecondaryHookState {
702            inner: secondary_db,
703            primary,
704            config,
705            is_fully_populated: AtomicBool::new(true),
706        });
707
708        // v1.6 (audit C3): register the secondary on the primary so
709        // future `Database::put` / `Database::delete` calls fan out to
710        // it automatically.  We downgrade to `Weak` so dropping the
711        // `SecondaryDatabase` handle removes it from the registry on
712        // the next iteration.
713        {
714            let weak: std::sync::Weak<dyn SecondaryHook + Send + Sync> =
715                Arc::downgrade(&state) as _;
716            state.primary.lock().register_secondary(weak);
717        }
718
719        // v1.6 (audit C2 / Decision 2C): if the secondary references a
720        // foreign primary DB, register as an FK referrer there so its
721        // `Database::delete` can call back into us with the configured
722        // ForeignKeyDeleteAction.
723        if let Some(fk_handle) = fk_handle {
724            let weak: std::sync::Weak<dyn FkReferrer + Send + Sync> =
725                Arc::downgrade(&state) as _;
726            fk_handle.lock().register_fk_referrer(weak);
727        }
728
729        let sec = SecondaryDatabase { state };
730
731        // If allow_populate and the secondary is empty, populate from primary.
732        if sec.state.config.allow_populate {
733            sec.populate_if_empty()?;
734        }
735
736        Ok(sec)
737    }
738
739    // ------------------------------------------------------------------
740    // Public API
741    // ------------------------------------------------------------------
742
743    /// Returns the database name of the secondary index.
744    pub fn get_database_name(&self) -> &str {
745        self.state.inner.get_database_name()
746    }
747
748    /// Returns the secondary configuration.
749    ///
750    ///
751    pub fn get_config(&self) -> &SecondaryConfig {
752        &self.state.config
753    }
754
755    /// Returns whether this handle is open.
756    pub fn is_valid(&self) -> bool {
757        self.state.inner.is_valid()
758    }
759
760    /// Closes the secondary database handle.
761    ///
762    ///
763    pub fn close(&self) -> Result<()> {
764        self.state.inner.close()
765    }
766
767    /// Returns the number of records in the secondary index.
768    ///
769    /// Equivalent to `Database::count` on the underlying inner index
770    /// database; included on `SecondaryDatabase` for symmetry with JE's
771    /// `SecondaryDatabase.count()` method.  See
772    /// (secondary-join “missing count/exists/truncate” Low).
773    ///
774    /// # Errors
775    /// Returns [`NoxuError::DatabaseClosed`] if the secondary handle has
776    /// been closed.
777    pub fn count(&self) -> Result<u64> {
778        self.state.inner.count()
779    }
780
781    /// Returns `true` if any record with the given secondary key exists.
782    ///
783    /// This avoids the cost of reading the primary record — unlike
784    /// [`Self::get`], which traverses the secondary, then the primary
785    /// database.  Useful for membership probes inside hot paths.
786    ///
787    /// # Errors
788    /// Propagates any error from the underlying secondary lookup.
789    pub fn exists(
790        &self,
791        txn: Option<&Transaction>,
792        key: &DatabaseEntry,
793    ) -> Result<bool> {
794        let mut data = DatabaseEntry::new();
795        let status = self.state.inner.get(txn, key, &mut data)?;
796        Ok(status == OperationStatus::Success)
797    }
798
799    /// Removes every record from the secondary index, leaving the
800    /// associated primary database untouched.
801    ///
802    /// **Caveat.** Truncating a secondary index without re-running
803    /// `populate_if_empty` (or replaying the primary-side updates)
804    /// leaves the secondary in a state that is not consistent with the
805    /// primary.  Most callers should drop the secondary's primary keys
806    /// via `Database::truncate_database` on the inner DB or repopulate
807    /// the index afterwards.  Returned for symmetry with JE's
808    /// `SecondaryDatabase.truncate(...)`.
809    ///
810    /// Returns the number of records that were in the index before the
811    /// truncate.
812    ///
813    /// # Errors
814    /// Returns [`NoxuError::DatabaseClosed`] if the secondary handle has
815    /// been closed, or any error returned by the underlying delete
816    /// loop.
817    pub fn truncate(&self) -> Result<u64> {
818        let pre = self.count()?;
819        // Walk every (sec_key, pri_key) pair via a primary-table-style
820        // scan and delete each.  The inner index is an ordinary
821        // Database, so this is just a cursor scan + delete.
822        let mut cursor = self.state.inner.open_cursor(None, None)?;
823        let mut sec_key = DatabaseEntry::new();
824        let mut data = DatabaseEntry::new();
825        // get_first returns NotFound if the index is empty.
826        if cursor.get(&mut sec_key, &mut data, crate::get::Get::First, None)?
827            != OperationStatus::Success
828        {
829            return Ok(0);
830        }
831        loop {
832            cursor.delete()?;
833            match cursor.get(
834                &mut sec_key,
835                &mut data,
836                crate::get::Get::Next,
837                None,
838            )? {
839                OperationStatus::Success => continue,
840                _ => break,
841            }
842        }
843        Ok(pre)
844    }
845
846    /// Retrieves a primary record by secondary key.
847    ///
848    ///
849    ///
850    /// Looks up `key` in the secondary index, obtains the primary key stored
851    /// there, then fetches the corresponding record from the primary database.
852    ///
853    /// # Arguments
854    /// * `txn` - Optional transaction.
855    /// * `key` - The secondary key to search for.
856    /// * `p_key` - Output: receives the primary key found.
857    /// * `data` - Output: receives the primary record data.
858    ///
859    /// # Returns
860    /// `OperationStatus::Success` if found; `OperationStatus::NotFound` otherwise.
861    pub fn get(
862        &self,
863        txn: Option<&Transaction>,
864        key: &DatabaseEntry,
865        p_key: &mut DatabaseEntry,
866        data: &mut DatabaseEntry,
867    ) -> Result<OperationStatus> {
868        self.check_open()?;
869        self.check_readable()?;
870
871        // Look up the secondary key in the index to get the primary key.
872        let mut pri_key_entry = DatabaseEntry::new();
873        let status = self.state.inner.get(txn, key, &mut pri_key_entry)?;
874
875        if status != OperationStatus::Success {
876            return Ok(OperationStatus::NotFound);
877        }
878
879        // Store the primary key in the output parameter.
880        if let Some(pk) = pri_key_entry.get_data() {
881            p_key.set_data(pk);
882        }
883
884        // Now fetch the primary record.
885        let primary = self.state.primary.lock();
886        let pri_status = primary.get(txn, &pri_key_entry, data)?;
887        if pri_status != OperationStatus::Success {
888            // Secondary refers to a missing primary — integrity issue.
889            return Err(NoxuError::SecondaryIntegrityException(format!(
890                "Secondary '{}' refers to missing primary key",
891                self.get_database_name()
892            )));
893        }
894
895        Ok(OperationStatus::Success)
896    }
897
898    /// Deletes all primary records whose secondary key equals `key`.
899    ///
900    ///
901    ///
902    /// All duplicate secondary index entries with the given secondary key are
903    /// found and their corresponding primary records deleted.  Each primary
904    /// deletion in turn removes all secondary index entries for that primary
905    /// record.
906    ///
907    /// # Arguments
908    /// * `txn` - Optional transaction.
909    /// * `key` - The secondary key whose primary records should be deleted.
910    ///
911    /// # Returns
912    /// `OperationStatus::Success` if at least one record was deleted;
913    /// `OperationStatus::NotFound` if the key was not found.
914    pub fn delete(
915        &self,
916        txn: Option<&Transaction>,
917        key: &DatabaseEntry,
918    ) -> Result<OperationStatus> {
919        self.check_open()?;
920
921        // Use a secondary cursor (under the caller's txn so the scan
922        // participates in the user's transaction) to iterate all
923        // duplicates of the secondary key.
924        let mut sec_cursor = self.open_cursor_internal(txn)?;
925        let mut p_key = DatabaseEntry::new();
926        let mut data = DatabaseEntry::new();
927
928        // Position to the first record with this secondary key.
929        let status = sec_cursor.get_search_key(key, &mut p_key, &mut data)?;
930
931        if status != OperationStatus::Success {
932            return Ok(OperationStatus::NotFound);
933        }
934
935        // We found at least one; iterate and delete all matching primary records.
936        loop {
937            let pri_key_bytes = p_key.get_data().unwrap_or(&[]).to_vec();
938            let pri_key_entry = DatabaseEntry::from_bytes(&pri_key_bytes);
939
940            // 1. Remove all secondary entries for this primary record first.
941            //    This includes the current secondary key entry we found.
942            //    UpdateSecondaryOnDelete calls updateSecondary.  Sprint 4½
943            //    forwards `txn` so the cleanup is atomic with the primary
944            //    delete below.
945            let old_data = data.clone();
946            self.delete_all_for_primary(txn, &pri_key_entry, Some(&old_data))?;
947
948            // 2. Delete the primary record.
949            {
950                let primary = self.state.primary.lock();
951                let _ = primary.delete(txn, &pri_key_entry)?;
952            }
953
954            // Re-search for the key to find any remaining duplicates.
955            // Since delete_all_for_primary cleaned up secondary entries,
956            // this should return NotFound when no more duplicates exist.
957            p_key = DatabaseEntry::new();
958            data = DatabaseEntry::new();
959            let next_status =
960                sec_cursor.get_search_key(key, &mut p_key, &mut data)?;
961            if next_status != OperationStatus::Success {
962                break;
963            }
964        }
965
966        Ok(OperationStatus::Success)
967    }
968
969    /// Opens a cursor on the secondary database.
970    ///
971    /// When `txn` is `Some(_)`, the inner cursor over the secondary
972    /// index participates in the supplied transaction — reads acquire
973    /// shared locks via the txn's locker and writes acquire exclusive
974    /// locks tracked by the txn.  Secondary cursors also
975    /// this to the *primary* lookups and the
976    /// [`SecondaryCursor::delete`] cascade as well: the cursor stores
977    /// the txn handle and forwards it to every primary `get` /
978    /// `delete` and to `delete_all_for_primary`.  Aborting the txn
979    /// rolls back **both** the secondary entry and the primary record
980    /// removed by `SecondaryCursor::delete` (and every secondary
981    /// cleanup it triggers).  When `txn` is `None`, every operation
982    /// runs auto-committed, matching the v1.4 behaviour.
983    ///
984    /// `config` is forwarded to the inner `Database::open_cursor` call so
985    /// `read_uncommitted` and other cursor-level flags propagate correctly.
986    ///
987    /// # Lifetime contract
988    ///
989    /// The returned [`SecondaryCursor`] borrows both the
990    /// `SecondaryDatabase` and — when supplied — the `Transaction`,
991    /// because primary deletes and cleanup writes are deferred until
992    /// `SecondaryCursor::delete` is called.  Callers must therefore
993    /// keep the `Transaction` alive at least as long as the cursor.
994    /// In practice this is the same lifetime rule that already applies
995    /// to [`Database::open_cursor`]; it is now enforced statically by
996    /// the type system.
997    ///
998    /// # Returns
999    /// A `SecondaryCursor` that iterates secondary index entries and returns
1000    /// primary data.
1001    pub fn open_cursor<'a>(
1002        &'a self,
1003        txn: Option<&'a Transaction>,
1004        config: Option<&CursorConfig>,
1005    ) -> Result<SecondaryCursor<'a>> {
1006        self.check_open()?;
1007        self.check_readable()?;
1008        SecondaryCursor::new(self, txn, config)
1009    }
1010
1011    /// Starts incremental population mode.
1012    ///
1013    ///
1014    pub fn start_incremental_population(&self) {
1015        self.state.is_fully_populated.store(false, Ordering::Release);
1016    }
1017
1018    /// Ends incremental population mode.
1019    ///
1020    ///
1021    pub fn end_incremental_population(&self) {
1022        self.state.is_fully_populated.store(true, Ordering::Release);
1023    }
1024
1025    /// Returns whether incremental population is currently enabled.
1026    ///
1027    ///
1028    pub fn is_incremental_population_enabled(&self) -> bool {
1029        !self.state.is_fully_populated.load(Ordering::Acquire)
1030    }
1031
1032    // ------------------------------------------------------------------
1033    // Internal helpers called by Database and SecondaryCursor
1034    // ------------------------------------------------------------------
1035
1036    /// Updates the secondary index when a primary record is inserted or updated.
1037    ///
1038    /// Called from application code that manages secondary index updates
1039    /// manually (v1.5 has no automatic `associate()`-style hook — that is
1040    /// v1.6 work).
1041    ///
1042    /// # Atomicity
1043    ///
1044    /// When `txn` is `Some(&t)`, **all** I/O performed by this method
1045    /// (cursor opens, `insert_sec_key`, `delete_sec_key`) is executed
1046    /// under `t`.  If the caller used the same `t` for the primary
1047    /// [`Database::put`] / [`Database::delete`] that prompted this
1048    /// update, the primary write and every affected secondary index
1049    /// entry commit or abort together.  This is the recommended
1050    /// pattern; see `docs/src/transactions/secondary-with-txn.md`.
1051    ///
1052    /// When `txn` is `None`, every inner secondary write runs
1053    /// auto-committed (v1.4 behaviour).  This is intentionally
1054    /// available so callers that do not need cross-database atomicity
1055    /// — e.g. one-shot population or single-threaded scripts — do not
1056    /// need to allocate a transaction.
1057    ///
1058    /// **Idempotent re-insert** (Decision 1B): if `update_secondary` is
1059    /// invoked twice with the same `(sec_key, pri_key)` pair (whether
1060    /// auto-commit or under the same `txn`), the second call is a
1061    /// no-op rather than a [`NoxuError::Unsupported`] collision — see
1062    /// `Self::insert_sec_key`.
1063    ///
1064    /// # Arguments
1065    /// * `txn` - Optional transaction.  Pass the same handle that
1066    ///   drives the primary write to make both updates atomic.
1067    /// * `pri_key` - The primary key.
1068    /// * `old_data` - The previous primary data, or `None` on insert.
1069    /// * `new_data` - The new primary data, or `None` on delete.
1070    pub fn update_secondary(
1071        &self,
1072        txn: Option<&Transaction>,
1073        pri_key: &DatabaseEntry,
1074        old_data: Option<&DatabaseEntry>,
1075        new_data: Option<&DatabaseEntry>,
1076    ) -> Result<()> {
1077        // Delegated to the state so the [`SecondaryHook`] trait impl can
1078        // share the same body when `Database::put` / `Database::delete`
1079        // drives automatic maintenance (audit C3).
1080        self.state.update_secondary(txn, pri_key, old_data, new_data)
1081    }
1082
1083    /// Removes all secondary index entries for the given primary key.
1084    ///
1085    /// Called when a primary record is deleted.  `txn` is forwarded to
1086    /// [`Self::update_secondary`] so the cleanup participates in the
1087    /// caller's transaction.
1088    pub(crate) fn delete_all_for_primary(
1089        &self,
1090        txn: Option<&Transaction>,
1091        pri_key: &DatabaseEntry,
1092        old_data: Option<&DatabaseEntry>,
1093    ) -> Result<()> {
1094        self.state.update_secondary(txn, pri_key, old_data, None)
1095    }
1096
1097    /// Returns a reference to the inner index `Database`.
1098    pub(crate) fn inner_db(&self) -> &Database {
1099        &self.state.inner
1100    }
1101
1102    /// Returns a reference to the primary `Database` (via the mutex).
1103    pub(crate) fn primary_db(&self) -> &Arc<Mutex<Database>> {
1104        &self.state.primary
1105    }
1106
1107    // ------------------------------------------------------------------
1108    // Private helpers
1109    // ------------------------------------------------------------------
1110
1111    /// Inserts a secondary index entry: (sec_key -> pri_key).
1112    ///
1113    /// v1.6 (Decision 1B / audit C4): the inner index DB is sorted-dup,
1114    /// so multiple primary records that produce the same `sec_key` are
1115    /// stored as duplicates of `sec_key`.  Delegates to the state-side
1116    /// implementation so the [`SecondaryHook`] trait shares it.
1117    fn insert_sec_key(
1118        &self,
1119        txn: Option<&Transaction>,
1120        sec_key: &DatabaseEntry,
1121        pri_key: &DatabaseEntry,
1122    ) -> Result<()> {
1123        self.state.insert_sec_key(txn, sec_key, pri_key)
1124    }
1125
1126    /// Deletes a secondary index entry: (sec_key -> pri_key).  Delegates
1127    /// to the state-side implementation.
1128    #[allow(dead_code)]
1129    fn delete_sec_key(
1130        &self,
1131        txn: Option<&Transaction>,
1132        sec_key: &DatabaseEntry,
1133        pri_key: &DatabaseEntry,
1134    ) -> Result<()> {
1135        self.state.delete_sec_key(txn, sec_key, pri_key)
1136    }
1137
1138    /// Builds a writable `Cursor` on the inner secondary index `Database`.
1139    /// Delegates to the state-side implementation.
1140    #[allow(dead_code)]
1141    fn make_inner_cursor(&self, txn: Option<&Transaction>) -> Result<Cursor> {
1142        self.state.inner.open_cursor(txn, None)
1143    }
1144
1145    /// Builds a `SecondaryCursor` on this secondary database (internal).
1146    ///
1147    /// `txn` is forwarded to [`SecondaryCursor::new`] so all inner-database
1148    /// reads and the cascade primary delete participate in the caller's
1149    /// transaction.  Used from
1150    /// [`SecondaryDatabase::delete`] to drive the secondary scan under
1151    /// the caller's txn.
1152    fn open_cursor_internal<'a>(
1153        &'a self,
1154        txn: Option<&'a Transaction>,
1155    ) -> Result<SecondaryCursor<'a>> {
1156        SecondaryCursor::new(self, txn, None)
1157    }
1158
1159    /// Populates the secondary index from the primary if the secondary is empty.
1160    ///
1161    /// Population logic in `SecondaryDatabase.init`.
1162    fn populate_if_empty(&self) -> Result<()> {
1163        // Check if the secondary is empty.
1164        let sec_count = self.state.inner.count()?;
1165        if sec_count > 0 {
1166            return Ok(());
1167        }
1168
1169        // Use direct CursorImpl scan to access both key and value.
1170        let primary = self.state.primary.lock();
1171        self.populate_from_primary_scan(&primary)?;
1172
1173        Ok(())
1174    }
1175
1176    /// Scans the primary database and inserts secondary index entries.
1177    fn populate_from_primary_scan(&self, primary: &Database) -> Result<()> {
1178        // We access the inner DatabaseImpl directly to read both key and value.
1179        // The public Cursor::get API currently only returns data, not key.
1180        // Use a dedicated scan loop via CursorImpl.
1181        let mut cursor = CursorImpl::new(Arc::clone(&primary.db_impl), 0);
1182
1183        let mut first_status = cursor
1184            .get_first()
1185            .map_err(|e| NoxuError::OperationNotAllowed(e.to_string()))?;
1186
1187        while first_status == noxu_dbi::OperationStatus::Success {
1188            let (k, v) = cursor
1189                .get_current()
1190                .map_err(|e| NoxuError::OperationNotAllowed(e.to_string()))?;
1191
1192            let pri_key = DatabaseEntry::from_bytes(&k);
1193            let pri_data = DatabaseEntry::from_bytes(&v);
1194
1195            // Create secondary key(s) and insert them.  Population runs
1196            // at `SecondaryDatabase::open` time, before any user txn
1197            // exists, so we auto-commit each insert (`txn = None`).
1198            if let Some(creator) = &self.state.config.key_creator {
1199                let mut sec_key = DatabaseEntry::new();
1200                if creator.create_secondary_key(
1201                    &self.state.inner,
1202                    &pri_key,
1203                    &pri_data,
1204                    &mut sec_key,
1205                ) {
1206                    self.insert_sec_key(None, &sec_key, &pri_key)?;
1207                }
1208            } else if let Some(multi_creator) =
1209                &self.state.config.multi_key_creator
1210            {
1211                let mut sec_keys = Vec::new();
1212                multi_creator.create_secondary_keys(
1213                    &self.state.inner,
1214                    &pri_key,
1215                    &pri_data,
1216                    &mut sec_keys,
1217                );
1218                for sec_key in sec_keys {
1219                    self.insert_sec_key(None, &sec_key, &pri_key)?;
1220                }
1221            }
1222
1223            first_status = cursor
1224                .retrieve_next(GetMode::Next)
1225                .map_err(|e| NoxuError::OperationNotAllowed(e.to_string()))?;
1226        }
1227
1228        Ok(())
1229    }
1230
1231    /// Checks that this database is open.
1232    fn check_open(&self) -> Result<()> {
1233        if !self.state.inner.is_valid() {
1234            return Err(NoxuError::DatabaseClosed);
1235        }
1236        Ok(())
1237    }
1238
1239    /// Checks that this database is readable (not in incremental population mode).
1240    fn check_readable(&self) -> Result<()> {
1241        if !self.state.is_fully_populated.load(Ordering::Acquire) {
1242            return Err(NoxuError::OperationNotAllowed(
1243                "Incremental population is currently enabled".to_string(),
1244            ));
1245        }
1246        Ok(())
1247    }
1248}
1249
1250impl Drop for SecondaryDatabase {
1251    fn drop(&mut self) {
1252        let _ = self.close();
1253    }
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258    use super::*;
1259    use crate::database_config::DatabaseConfig;
1260    use crate::environment::Environment;
1261    use crate::environment_config::EnvironmentConfig;
1262    use crate::secondary_config::{SecondaryConfig, SecondaryKeyCreator};
1263    use tempfile::TempDir;
1264
1265    /// A simple key creator that uses the first byte of the value as the
1266    /// secondary key.
1267    struct FirstByteKeyCreator;
1268
1269    impl SecondaryKeyCreator for FirstByteKeyCreator {
1270        fn create_secondary_key(
1271            &self,
1272            _db: &Database,
1273            _key: &DatabaseEntry,
1274            data: &DatabaseEntry,
1275            result: &mut DatabaseEntry,
1276        ) -> bool {
1277            if let Some(d) = data.get_data()
1278                && !d.is_empty()
1279            {
1280                result.set_data(&d[..1]);
1281                return true;
1282            }
1283            false
1284        }
1285    }
1286
1287    fn temp_env() -> (TempDir, Environment) {
1288        let temp_dir = TempDir::new().unwrap();
1289        let env_config = EnvironmentConfig::new(temp_dir.path().to_path_buf())
1290            .with_allow_create(true)
1291            .with_transactional(true);
1292        let env = Environment::open(env_config).unwrap();
1293        (temp_dir, env)
1294    }
1295
1296    fn open_primary(env: &Environment, name: &str) -> Database {
1297        let config = DatabaseConfig::new().with_allow_create(true);
1298        env.open_database(None, name, &config).unwrap()
1299    }
1300
1301    fn open_secondary(
1302        primary: Arc<Mutex<Database>>,
1303        env: &Environment,
1304        name: &str,
1305    ) -> SecondaryDatabase {
1306        // v1.6 sorted-dup secondaries: inner index DB must allow dups.
1307        let sec_db_config = DatabaseConfig::new()
1308            .with_allow_create(true)
1309            .with_sorted_duplicates(true);
1310        let sec_db = env.open_database(None, name, &sec_db_config).unwrap();
1311        let sec_config = SecondaryConfig::new()
1312            .with_allow_create(true)
1313            .with_key_creator(Box::new(FirstByteKeyCreator));
1314        SecondaryDatabase::open(primary, sec_db, sec_config).unwrap()
1315    }
1316
1317    #[test]
1318    fn test_open_secondary() {
1319        let (_tmp, env) = temp_env();
1320        let primary = Arc::new(Mutex::new(open_primary(&env, "primary")));
1321        let secondary = open_secondary(Arc::clone(&primary), &env, "secondary");
1322        assert!(secondary.is_valid());
1323        assert_eq!(secondary.get_database_name(), "secondary");
1324    }
1325
1326    #[test]
1327    fn test_put_primary_updates_secondary() {
1328        let (_tmp, env) = temp_env();
1329        let primary = Arc::new(Mutex::new(open_primary(&env, "primary")));
1330        let secondary = open_secondary(Arc::clone(&primary), &env, "secondary");
1331
1332        // Write to primary; secondary is not auto-updated here because
1333        // Database::put does not know about secondaries by default.
1334        // We manually call update_secondary for this test.
1335        let pri_key = DatabaseEntry::from_bytes(b"pk1");
1336        let pri_data = DatabaseEntry::from_bytes(b"Avalon");
1337        {
1338            let primary = primary.lock();
1339            primary.put(None, &pri_key, &pri_data).unwrap();
1340        }
1341
1342        // Update the secondary index manually (mimics the integration layer).
1343        secondary
1344            .update_secondary(None, &pri_key, None, Some(&pri_data))
1345            .unwrap();
1346
1347        // Retrieve by secondary key (first byte of "Avalon" = 'A' = 0x41).
1348        let sec_key = DatabaseEntry::from_bytes(b"A");
1349        let mut p_key = DatabaseEntry::new();
1350        let mut data = DatabaseEntry::new();
1351        let status =
1352            secondary.get(None, &sec_key, &mut p_key, &mut data).unwrap();
1353
1354        assert_eq!(status, OperationStatus::Success);
1355        assert_eq!(p_key.get_data().unwrap(), b"pk1");
1356        assert_eq!(data.get_data().unwrap(), b"Avalon");
1357    }
1358
1359    #[test]
1360    fn test_get_by_secondary_key() {
1361        let (_tmp, env) = temp_env();
1362        let primary = Arc::new(Mutex::new(open_primary(&env, "primary")));
1363        let secondary = open_secondary(Arc::clone(&primary), &env, "secondary");
1364
1365        // Insert primary records and index them.  Each record uses a
1366        // distinct first byte so the v1.5 one-to-one secondary contract
1367        // (Decision 1B) is satisfied.
1368        let records: &[(&[u8], &[u8])] =
1369            &[(b"pk1", b"Apple"), (b"pk2", b"Banana"), (b"pk3", b"Cherry")];
1370
1371        for (k, v) in records {
1372            let pk = DatabaseEntry::from_bytes(k);
1373            let pv = DatabaseEntry::from_bytes(v);
1374            {
1375                primary.lock().put(None, &pk, &pv).unwrap();
1376            }
1377            secondary.update_secondary(None, &pk, None, Some(&pv)).unwrap();
1378        }
1379
1380        // Search by secondary key 'B'.
1381        let sec_key = DatabaseEntry::from_bytes(b"B");
1382        let mut p_key = DatabaseEntry::new();
1383        let mut data = DatabaseEntry::new();
1384        let status =
1385            secondary.get(None, &sec_key, &mut p_key, &mut data).unwrap();
1386
1387        assert_eq!(status, OperationStatus::Success);
1388        assert_eq!(data.get_data().unwrap(), b"Banana");
1389
1390        // Search for non-existent secondary key.
1391        let missing = DatabaseEntry::from_bytes(b"Z");
1392        let status =
1393            secondary.get(None, &missing, &mut p_key, &mut data).unwrap();
1394        assert_eq!(status, OperationStatus::NotFound);
1395    }
1396
1397    #[test]
1398    fn test_delete_via_secondary() {
1399        let (_tmp, env) = temp_env();
1400        let primary = Arc::new(Mutex::new(open_primary(&env, "primary")));
1401        let secondary = open_secondary(Arc::clone(&primary), &env, "secondary");
1402
1403        let pri_key = DatabaseEntry::from_bytes(b"pk1");
1404        let pri_data = DatabaseEntry::from_bytes(b"Cherry");
1405        {
1406            primary.lock().put(None, &pri_key, &pri_data).unwrap();
1407        }
1408        secondary
1409            .update_secondary(None, &pri_key, None, Some(&pri_data))
1410            .unwrap();
1411
1412        // Delete via secondary key.
1413        let sec_key = DatabaseEntry::from_bytes(b"C");
1414        let status = secondary.delete(None, &sec_key).unwrap();
1415        assert_eq!(status, OperationStatus::Success);
1416
1417        // Primary record should be gone.
1418        let mut data = DatabaseEntry::new();
1419        let get_status = primary.lock().get(None, &pri_key, &mut data).unwrap();
1420        assert_eq!(get_status, OperationStatus::NotFound);
1421    }
1422
1423    #[test]
1424    fn test_update_changes_secondary_key() {
1425        let (_tmp, env) = temp_env();
1426        let primary = Arc::new(Mutex::new(open_primary(&env, "primary")));
1427        let secondary = open_secondary(Arc::clone(&primary), &env, "secondary");
1428
1429        let pri_key = DatabaseEntry::from_bytes(b"pk1");
1430        let old_data = DatabaseEntry::from_bytes(b"Mango");
1431        let new_data = DatabaseEntry::from_bytes(b"Pineapple");
1432
1433        {
1434            primary.lock().put(None, &pri_key, &old_data).unwrap();
1435        }
1436        secondary
1437            .update_secondary(None, &pri_key, None, Some(&old_data))
1438            .unwrap();
1439
1440        // Now update the primary; the secondary key 'M' should be replaced by 'P'.
1441        {
1442            primary.lock().put(None, &pri_key, &new_data).unwrap();
1443        }
1444        secondary
1445            .update_secondary(None, &pri_key, Some(&old_data), Some(&new_data))
1446            .unwrap();
1447
1448        // Old key 'M' should no longer be in the secondary.
1449        let old_sec = DatabaseEntry::from_bytes(b"M");
1450        let mut pk = DatabaseEntry::new();
1451        let mut data = DatabaseEntry::new();
1452        let status = secondary.get(None, &old_sec, &mut pk, &mut data).unwrap();
1453        assert_eq!(status, OperationStatus::NotFound);
1454
1455        // New key 'P' should be present.
1456        let new_sec = DatabaseEntry::from_bytes(b"P");
1457        let status = secondary.get(None, &new_sec, &mut pk, &mut data).unwrap();
1458        assert_eq!(status, OperationStatus::Success);
1459        assert_eq!(data.get_data().unwrap(), b"Pineapple");
1460    }
1461
1462    #[test]
1463    fn test_cursor_scan_secondary() {
1464        let (_tmp, env) = temp_env();
1465        let primary = Arc::new(Mutex::new(open_primary(&env, "primary")));
1466        let secondary = open_secondary(Arc::clone(&primary), &env, "secondary");
1467
1468        // Insert records with distinct first bytes.
1469        let records: &[(&[u8], &[u8])] =
1470            &[(b"pk1", b"Banana"), (b"pk2", b"Cherry"), (b"pk3", b"Apple")];
1471        for (k, v) in records {
1472            let pk = DatabaseEntry::from_bytes(k);
1473            let pv = DatabaseEntry::from_bytes(v);
1474            primary.lock().put(None, &pk, &pv).unwrap();
1475            secondary.update_secondary(None, &pk, None, Some(&pv)).unwrap();
1476        }
1477
1478        // Iterate via SecondaryCursor and collect all secondary keys encountered.
1479        let mut cursor = secondary.open_cursor(None, None).unwrap();
1480        let mut sec_keys_seen: Vec<Vec<u8>> = Vec::new();
1481        let mut sec_key = DatabaseEntry::new();
1482        let mut p_key = DatabaseEntry::new();
1483        let mut data = DatabaseEntry::new();
1484
1485        let status =
1486            cursor.get_first(&mut sec_key, &mut p_key, &mut data).unwrap();
1487        let mut current = status;
1488        while current == OperationStatus::Success {
1489            if let Some(k) = sec_key.get_data() {
1490                sec_keys_seen.push(k.to_vec());
1491            }
1492            current =
1493                cursor.get_next(&mut sec_key, &mut p_key, &mut data).unwrap();
1494        }
1495
1496        // We expect 3 entries (A, B, C in secondary key order).
1497        assert_eq!(sec_keys_seen.len(), 3);
1498        assert_eq!(sec_keys_seen[0], b"A");
1499        assert_eq!(sec_keys_seen[1], b"B");
1500        assert_eq!(sec_keys_seen[2], b"C");
1501    }
1502
1503    #[test]
1504    fn test_incremental_population() {
1505        let (_tmp, env) = temp_env();
1506        let primary = Arc::new(Mutex::new(open_primary(&env, "primary")));
1507        let secondary = open_secondary(Arc::clone(&primary), &env, "secondary");
1508
1509        secondary.start_incremental_population();
1510        assert!(secondary.is_incremental_population_enabled());
1511
1512        // Reads should fail during incremental population.
1513        let sec_key = DatabaseEntry::from_bytes(b"A");
1514        let mut pk = DatabaseEntry::new();
1515        let mut data = DatabaseEntry::new();
1516        let result = secondary.get(None, &sec_key, &mut pk, &mut data);
1517        assert!(result.is_err());
1518
1519        secondary.end_incremental_population();
1520        assert!(!secondary.is_incremental_population_enabled());
1521    }
1522
1523    #[test]
1524    fn test_populate_on_open() {
1525        let (_tmp, env) = temp_env();
1526        let primary = Arc::new(Mutex::new(open_primary(&env, "primary")));
1527
1528        // Pre-populate the primary.
1529        let records: &[(&[u8], &[u8])] =
1530            &[(b"pk1", b"Grape"), (b"pk2", b"Watermelon")];
1531        for (k, v) in records {
1532            primary
1533                .lock()
1534                .put(
1535                    None,
1536                    &DatabaseEntry::from_bytes(k),
1537                    &DatabaseEntry::from_bytes(v),
1538                )
1539                .unwrap();
1540        }
1541
1542        // Open secondary with allow_populate=true.
1543        let sec_db_config = DatabaseConfig::new()
1544            .with_allow_create(true)
1545            .with_sorted_duplicates(true);
1546        let sec_db =
1547            env.open_database(None, "secondary_pop", &sec_db_config).unwrap();
1548        let sec_config = SecondaryConfig::new()
1549            .with_allow_create(true)
1550            .with_allow_populate(true)
1551            .with_key_creator(Box::new(FirstByteKeyCreator));
1552        let secondary =
1553            SecondaryDatabase::open(Arc::clone(&primary), sec_db, sec_config)
1554                .unwrap();
1555
1556        // The secondary should have been populated.
1557        let sec_key_g = DatabaseEntry::from_bytes(b"G");
1558        let mut pk = DatabaseEntry::new();
1559        let mut data = DatabaseEntry::new();
1560        let status =
1561            secondary.get(None, &sec_key_g, &mut pk, &mut data).unwrap();
1562        assert_eq!(status, OperationStatus::Success);
1563        assert_eq!(data.get_data().unwrap(), b"Grape");
1564    }
1565
1566    /// Note:
1567    /// Low) — the new convenience methods on SecondaryDatabase delegate
1568    /// to the inner index DB and surface the JE-shape API for the
1569    /// secondary side.
1570    #[test]
1571    fn test_count_exists_truncate_round_trip() {
1572        let (_tmp, env) = temp_env();
1573        let primary = Arc::new(Mutex::new(open_primary(&env, "pri")));
1574        let secondary = open_secondary(Arc::clone(&primary), &env, "sec");
1575
1576        // Empty index → count == 0, no key exists.
1577        assert_eq!(secondary.count().unwrap(), 0);
1578        assert!(
1579            !secondary.exists(None, &DatabaseEntry::from_bytes(b"A")).unwrap()
1580        );
1581
1582        // Populate three primaries with distinct first-byte secondary keys.
1583        for (pk, pv) in &[
1584            (&b"pk1"[..], &b"Apple"[..]),
1585            (&b"pk2"[..], &b"Banana"[..]),
1586            (&b"pk3"[..], &b"Cherry"[..]),
1587        ] {
1588            let pk_e = DatabaseEntry::from_bytes(pk);
1589            let pv_e = DatabaseEntry::from_bytes(pv);
1590            primary.lock().put(None, &pk_e, &pv_e).unwrap();
1591            secondary.update_secondary(None, &pk_e, None, Some(&pv_e)).unwrap();
1592        }
1593
1594        assert_eq!(secondary.count().unwrap(), 3);
1595        assert!(
1596            secondary.exists(None, &DatabaseEntry::from_bytes(b"A")).unwrap()
1597        );
1598        assert!(
1599            secondary.exists(None, &DatabaseEntry::from_bytes(b"C")).unwrap()
1600        );
1601        assert!(
1602            !secondary.exists(None, &DatabaseEntry::from_bytes(b"Z")).unwrap()
1603        );
1604
1605        // Truncate clears every record and reports the pre-truncate count.
1606        let removed = secondary.truncate().unwrap();
1607        assert_eq!(removed, 3);
1608        assert_eq!(secondary.count().unwrap(), 0);
1609        assert!(
1610            !secondary.exists(None, &DatabaseEntry::from_bytes(b"A")).unwrap()
1611        );
1612    }
1613}