Skip to main content

reddb_server/runtime/
impl_lifecycle.rs

1//! Runtime bootstrap / constructor / handle accessors.
2//!
3//! Extracted verbatim from `impl_core.rs` (impl_core slice 8/10, issue #1629).
4//! Houses the in-memory / options constructors, the ~800-line `with_pool`
5//! constructor (moved verbatim — restructuring it is future work), snapshot
6//! rehydration and materialized-view backing lifecycle, system keyed-collection
7//! bootstrap, and the db / index-store / schema-vocabulary / auth-store /
8//! oauth / browser-token / registry handle accessors.
9//!
10//! - **Free helpers** — `view_records_to_entities`,
11//!   `system_keyed_collection_contract`, `table_row_index_fields`.
12use super::impl_config_secret::seed_storage_deploy_config;
13use super::*;
14
15/// Convert the rows produced by a materialized-view body into
16/// `UnifiedEntity` table rows targeting the backing collection.
17/// Issue #595 slice 9c — feeds `UnifiedStore::refresh_collection`.
18///
19/// Graph fragments and vector hits are ignored: a materialized view
20/// is a relational result set (SELECT-shaped); slices 11+ may extend
21/// this once we have a richer view body shape. Each row materialises
22/// the union of its schema-bound columns + overflow.
23pub(crate) fn view_records_to_entities(
24    table: &str,
25    records: &[crate::storage::query::unified::UnifiedRecord],
26) -> Vec<crate::storage::UnifiedEntity> {
27    use std::collections::HashMap;
28    let table_arc: std::sync::Arc<str> = std::sync::Arc::from(table);
29    let mut out = Vec::with_capacity(records.len());
30    for record in records {
31        let mut named: HashMap<String, reddb_types::Value> = HashMap::new();
32        for (name, value) in record.iter_fields() {
33            named.insert(name.to_string(), value.clone());
34        }
35        let entity = crate::storage::UnifiedEntity::new(
36            crate::storage::EntityId::new(0),
37            crate::storage::EntityKind::TableRow {
38                table: std::sync::Arc::clone(&table_arc),
39                row_id: 0,
40            },
41            crate::storage::EntityData::Row(crate::storage::RowData {
42                columns: Vec::new(),
43                named: Some(named),
44                schema: None,
45            }),
46        );
47        out.push(entity);
48    }
49    out
50}
51
52fn system_keyed_collection_contract(
53    name: &str,
54    model: crate::catalog::CollectionModel,
55) -> crate::physical::CollectionContract {
56    let now = crate::utils::now_unix_millis() as u128;
57    crate::physical::CollectionContract {
58        name: name.to_string(),
59        declared_model: model,
60        schema_mode: crate::catalog::SchemaMode::Dynamic,
61        origin: crate::physical::ContractOrigin::Implicit,
62        version: 1,
63        created_at_unix_ms: now,
64        updated_at_unix_ms: now,
65        default_ttl_ms: None,
66        vector_dimension: None,
67        vector_metric: None,
68        context_index_fields: Vec::new(),
69        declared_columns: Vec::new(),
70        table_def: None,
71        timestamps_enabled: false,
72        context_index_enabled: false,
73        metrics_raw_retention_ms: None,
74        metrics_rollup_policies: Vec::new(),
75        metrics_tenant_identity: None,
76        metrics_namespace: None,
77        append_only: false,
78        subscriptions: Vec::new(),
79        analytics_config: Vec::new(),
80        session_key: None,
81        session_gap_ms: None,
82        retention_duration_ms: None,
83        analytical_storage: None,
84
85        ai_policy: None,
86    }
87}
88
89pub(crate) fn table_row_index_fields(
90    entity: &crate::storage::unified::entity::UnifiedEntity,
91) -> Vec<(String, reddb_types::Value)> {
92    let crate::storage::EntityData::Row(row) = &entity.data else {
93        return Vec::new();
94    };
95    if let Some(named) = &row.named {
96        return named
97            .iter()
98            .map(|(name, value)| (name.clone(), value.clone()))
99            .collect();
100    }
101    if let Some(schema) = &row.schema {
102        return schema
103            .iter()
104            .zip(row.columns.iter())
105            .map(|(name, value)| (name.clone(), value.clone()))
106            .collect();
107    }
108    Vec::new()
109}
110
111impl RedDBRuntime {
112    pub fn in_memory() -> RedDBResult<Self> {
113        Self::with_options(RedDBOptions::in_memory())
114    }
115
116    pub fn flush(&self) -> RedDBResult<()> {
117        let store = self.inner.db.store();
118        let publication = store.vault_publication_lock.lock();
119        self.inner
120            .db
121            .flush_local_only()
122            .map_err(|err| RedDBError::Internal(err.to_string()))?;
123        self.index_store_ref().clear_paged_btree_overlays();
124        drop(publication);
125        self.inner
126            .db
127            .upload_to_remote_backend()
128            .map_err(|err| RedDBError::Internal(err.to_string()))
129    }
130
131    /// Handle to the intent-lock manager for tests + introspection.
132    /// Production code acquires via `LockerGuard::new(rt.lock_manager())`
133    /// rather than touching the manager directly.
134    pub fn lock_manager(&self) -> std::sync::Arc<crate::runtime::lock_manager::LockManager> {
135        self.inner.lock_manager.clone()
136    }
137
138    /// Process-local governance registry for managed policy/config guardrails.
139    pub fn config_registry(&self) -> std::sync::Arc<crate::auth::registry::ConfigRegistry> {
140        self.inner.config_registry.clone()
141    }
142
143    pub fn query_audit(&self) -> std::sync::Arc<crate::runtime::query_audit::QueryAuditStream> {
144        self.inner.query_audit.clone()
145    }
146
147    pub fn control_events_require_persistence(&self) -> bool {
148        self.inner.control_event_config.require_persistence()
149    }
150
151    pub fn control_event_config(&self) -> crate::runtime::control_events::ControlEventConfig {
152        self.inner.control_event_config
153    }
154
155    pub fn control_event_ledger(
156        &self,
157    ) -> Arc<dyn crate::runtime::control_events::ControlEventLedger> {
158        self.inner.control_event_ledger.read().clone()
159    }
160
161    #[doc(hidden)]
162    pub fn replace_control_event_ledger_for_tests(
163        &self,
164        ledger: Arc<dyn crate::runtime::control_events::ControlEventLedger>,
165    ) {
166        *self.inner.control_event_ledger.write() = ledger;
167    }
168
169    #[inline(never)]
170    pub fn with_options(options: RedDBOptions) -> RedDBResult<Self> {
171        Self::with_pool(options, ConnectionPoolConfig::default())
172    }
173
174    /// The memory budget resolved at boot (ADR 0073 §1). Immutable for the
175    /// process lifetime; echoed by the boot log and the `red.stats` budget
176    /// section.
177    pub fn memory_budget(&self) -> crate::storage::memory_budget::MemoryBudget {
178        self.inner.memory_budget
179    }
180
181    /// The one shared accounting pool (ADR 0073 §2). Per-pool shares are fixed
182    /// at boot; live usage is refreshed by
183    /// [`RedDBRuntime::refresh_memory_accounting`].
184    pub fn memory_accounting(&self) -> &crate::storage::memory_pools::MemoryAccounting {
185        &self.inner.memory_accounting
186    }
187
188    pub fn with_pool(
189        options: RedDBOptions,
190        pool_config: ConnectionPoolConfig,
191    ) -> RedDBResult<Self> {
192        Self::with_pool_and_limits(
193            options,
194            pool_config,
195            super::resource_limits::ResourceLimits::from_env(),
196        )
197    }
198
199    pub(super) fn with_pool_and_limits(
200        options: RedDBOptions,
201        pool_config: ConnectionPoolConfig,
202        resource_limits: super::resource_limits::ResourceLimits,
203    ) -> RedDBResult<Self> {
204        options
205            .replication
206            .validate_failover_profile()
207            .map_err(RedDBError::InvalidConfig)?;
208        // PLAN.md Phase 9.1 — capture wall-clock before storage
209        // open so the cold-start phase markers can be backfilled
210        // once Lifecycle is constructed below. Storage open
211        // encapsulates auto-restore + WAL replay; we treat the
212        // whole window as one combined "restore" + "wal_replay"
213        // phase split at the same boundary because the storage
214        // layer doesn't yet emit a finer signal.
215        let boot_open_start_ms = std::time::SystemTime::now()
216            .duration_since(std::time::UNIX_EPOCH)
217            .map(|d| d.as_millis() as u64)
218            .unwrap_or(0);
219        // ADR 0073 §1 — resolve the one memory budget before anything is
220        // opened, so a nonsensical operator value fails the boot before the
221        // process touches disk. The log line fires once per process.
222        let memory_budget = crate::storage::memory_budget::resolve_for_boot(
223            options.storage_profile.deploy_profile,
224            options.memory_budget_bytes,
225        )
226        .map_err(|err| RedDBError::InvalidConfig(err.to_string()))?;
227        crate::storage::memory_budget::log_resolved_once(&memory_budget);
228        // ADR 0073 §2 — divide the budget among the pools before any of them
229        // is constructed. `BudgetShares::resolve` asserts Σ(shares) ≤ budget;
230        // one boot log line per pool names its share.
231        let memory_shares = crate::storage::memory_pools::BudgetShares::resolve(
232            memory_budget,
233            options.storage_profile.deploy_profile,
234        );
235        memory_shares.log_once();
236        let embedded_single_file = options.storage_profile.deploy_profile
237            == crate::storage::DeployProfile::Embedded
238            && options.storage_profile.packaging == crate::storage::StoragePackaging::SingleFile;
239        let db = Arc::new(RedDB::open_with_options(&options).map_err(|err| {
240            match err.downcast::<crate::storage::StoreError>() {
241                Ok(store_err) => RedDBError::from(*store_err),
242                Err(err) => RedDBError::Internal(err.to_string()),
243            }
244        })?);
245        // Query results are disposable. Do not create a second persistent
246        // database outside the .rdb and its WAL for the cache.
247        let result_blob_cache_config = crate::storage::cache::BlobCacheConfig::default()
248            .with_l1_bytes_max(memory_shares.blob_cache_l1_bytes());
249        let result_blob_cache =
250            crate::storage::cache::BlobCache::open_with_l2(result_blob_cache_config).map_err(
251                |err| RedDBError::Internal(format!("open result Blob Cache L2 failed: {err:?}")),
252            )?;
253        let storage_ready_ms = std::time::SystemTime::now()
254            .duration_since(std::time::UNIX_EPOCH)
255            .map(|d| d.as_millis() as u64)
256            .unwrap_or(0);
257
258        let no_auth_boot = crate::service_cli::no_auth_active(&options);
259        let runtime = Self {
260            background_tasks: Some(Arc::new(background_tasks::RuntimeBackgroundTasks::default())),
261            inner: Arc::new(RuntimeInner {
262                functions: parking_lot::RwLock::new(
263                    super::function_catalog::FunctionCatalog::load(&db)?,
264                ),
265                db: db.clone(),
266                layout: PhysicalLayout::from_options(&options),
267                embedded_single_file,
268                memory_budget,
269                memory_accounting: Arc::new(
270                    crate::storage::memory_pools::MemoryAccounting::from_shares(
271                        memory_budget,
272                        memory_shares,
273                    ),
274                ),
275                memory_reservations: parking_lot::Mutex::new(
276                    super::memory_admission::MemoryReservations::default(),
277                ),
278                mvcc_maintenance: parking_lot::Mutex::new(Default::default()),
279                indices: IndexCatalog::register_default_vector_graph(
280                    options.has_capability(crate::api::Capability::Table),
281                    options.has_capability(crate::api::Capability::Graph),
282                ),
283                pool_config,
284                pool: Mutex::new(PoolState::default()),
285                execution_admission: parking_lot::RwLock::new(
286                    crate::transport::execution_admission::ExecutionAdmission::for_host(),
287                ),
288                execution_queue_timeout_ms: AtomicU64::new(30_000),
289                started_at_unix_ms: SystemTime::now()
290                    .duration_since(UNIX_EPOCH)
291                    .unwrap_or_default()
292                    .as_millis(),
293                probabilistic: super::probabilistic_store::ProbabilisticStore::new(),
294                index_store: super::index_store::IndexStore::new(),
295                cdc: crate::replication::cdc::CdcBuffer::new(100_000),
296                checkpoint_projection_stats: super::CheckpointProjectionStats::default(),
297                scrub_state: parking_lot::Mutex::new(super::ScrubRuntimeState::default()),
298                checkpoint_columnar_emission_budget_chunks: options
299                    .checkpoint_columnar_emission_budget_chunks,
300                columnar_projection_size_floor_rows: options.columnar_projection_size_floor_rows,
301                backup_scheduler: crate::replication::scheduler::BackupScheduler::new(3600),
302                query_cache: parking_lot::RwLock::new(
303                    crate::storage::query::planner::cache::PlanCache::new(1000),
304                ),
305                result_cache: parking_lot::RwLock::new((
306                    HashMap::new(),
307                    std::collections::VecDeque::new(),
308                )),
309                result_blob_cache,
310                result_blob_entries: parking_lot::RwLock::new((
311                    HashMap::new(),
312                    std::collections::VecDeque::new(),
313                )),
314                ask_answer_cache_entries: parking_lot::RwLock::new((
315                    HashSet::new(),
316                    std::collections::VecDeque::new(),
317                )),
318                result_cache_shadow_divergences: std::sync::atomic::AtomicU64::new(0),
319                result_cache_hits: std::sync::atomic::AtomicU64::new(0),
320                result_cache_misses: std::sync::atomic::AtomicU64::new(0),
321                result_cache_evictions: std::sync::atomic::AtomicU64::new(0),
322                ask_daily_spend: parking_lot::RwLock::new(HashMap::new()),
323                queue_message_locks: parking_lot::RwLock::new(HashMap::new()),
324                rmw_locks: RmwLockTable::new(),
325                planner_dirty_tables: parking_lot::RwLock::new(HashSet::new()),
326                ec_registry: Arc::new(crate::ec::config::EcRegistry::new()),
327                config_registry: Arc::new(crate::auth::registry::ConfigRegistry::new()),
328                ec_worker: crate::ec::worker::EcWorker::new(),
329                auth_store: parking_lot::RwLock::new(None),
330                oauth_validator: parking_lot::RwLock::new(None),
331                browser_token_authority: parking_lot::RwLock::new(None),
332                views: parking_lot::RwLock::new(HashMap::new()),
333                materialized_views: parking_lot::RwLock::new(
334                    crate::storage::cache::result::MaterializedViewCache::new(),
335                ),
336                retention_sweeper: parking_lot::RwLock::new(
337                    crate::runtime::retention_sweeper::RetentionSweeperState::new(),
338                ),
339                transaction_state: crate::runtime::transaction_state::TransactionState::new(),
340                env_config_overrides: crate::runtime::config_overlay::collect_env_overrides(),
341                lock_manager: Arc::new({
342                    // Sourced from the matrix: Tier B key
343                    // `concurrency.locking.deadlock_timeout_ms`
344                    // (default 5000). Env var wins at boot so
345                    // operators can tune without touching red_config.
346                    let env = crate::runtime::config_overlay::collect_env_overrides();
347                    let timeout_ms = env
348                        .get("concurrency.locking.deadlock_timeout_ms")
349                        .and_then(|raw| raw.parse::<u64>().ok())
350                        .unwrap_or_else(|| {
351                            match crate::runtime::config_matrix::default_for(
352                                "concurrency.locking.deadlock_timeout_ms",
353                            ) {
354                                Some(crate::serde_json::Value::Number(n)) => n as u64,
355                                _ => 5000,
356                            }
357                        });
358                    let cfg = crate::runtime::lock_manager::LockConfig {
359                        default_timeout: std::time::Duration::from_millis(timeout_ms),
360                        ..Default::default()
361                    };
362                    crate::runtime::lock_manager::LockManager::new(cfg)
363                }),
364                rls_policies: parking_lot::RwLock::new(HashMap::new()),
365                rls_enabled_tables: parking_lot::RwLock::new(HashSet::new()),
366                foreign_tables: Arc::new(crate::storage::fdw::ForeignTableRegistry::with_builtins()),
367                pending_tombstones: parking_lot::RwLock::new(HashMap::new()),
368                pending_versioned_updates: parking_lot::RwLock::new(HashMap::new()),
369                pending_queue_dedup: parking_lot::RwLock::new(HashMap::new()),
370                pending_kv_watch_events: parking_lot::RwLock::new(HashMap::new()),
371                pending_vault_writes: parking_lot::RwLock::new(HashMap::new()),
372                pending_store_wal_actions: parking_lot::RwLock::new(HashMap::new()),
373                pending_claim_locks: parking_lot::RwLock::new(HashMap::new()),
374                queue_wait_registry: std::sync::Arc::new(
375                    crate::runtime::queue_wait_registry::QueueWaitRegistry::new(),
376                ),
377                pending_queue_wakes: parking_lot::RwLock::new(HashMap::new()),
378                tenant_tables: parking_lot::RwLock::new(HashMap::new()),
379                ddl_epoch: std::sync::atomic::AtomicU64::new(0),
380                write_gate: Arc::new(crate::runtime::write_gate::WriteGate::from_options(
381                    &options,
382                )),
383                lifecycle: crate::runtime::lifecycle::Lifecycle::new(),
384                resource_limits,
385                query_budget_slots: AtomicU64::new(0),
386                audit_log: {
387                    // Default audit-log path for the in-memory case
388                    // sits in the system temp dir; persistent runs
389                    // place it next to the resolved data file.
390                    //
391                    // gh-471 iter 2: route through the resolved
392                    // `LogDestination`. Performance/Max tiers emit a
393                    // file-backed log destination under the file-owned
394                    // support-directory logs tier;
395                    // lower tiers / ephemeral runs report `Stderr`
396                    // and we keep the legacy file-next-to-data sink.
397                    // #1375 — single-file embedded mode keeps the data
398                    // directory to exactly the `.rdb` artifact, so the audit
399                    // log must NOT land as a sibling. Route it to a
400                    // process-unique temp location even when a data path is
401                    // set; only the non-embedded case uses the data dir.
402                    let data_path = if embedded_single_file || options.read_only {
403                        std::env::temp_dir()
404                            .join("reddb-embedded-runtime")
405                            .join(format!("audit-{}", std::process::id()))
406                    } else {
407                        options
408                            .data_path
409                            .clone()
410                            .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
411                    };
412                    let (audit_dest, _) = crate::api::tier_wiring::current_log_destinations();
413                    let audit_dest = if options.read_only {
414                        crate::storage::layout::LogDestination::Stderr
415                    } else {
416                        audit_dest
417                    };
418                    if !matches!(audit_dest, crate::storage::layout::LogDestination::File(_))
419                        && (embedded_single_file
420                            || options.read_only
421                            || options
422                                .metadata
423                                .contains_key(crate::api::EPHEMERAL_RUNTIME_METADATA_KEY))
424                    {
425                        // The Stderr/Syslog lower-tier sink resolves to a
426                        // `for_data_path` sibling that collides across concurrent
427                        // temp-dir runtimes — nextest's process-per-test model
428                        // truncates one shared file, flaking audit assertions.
429                        // Pin a unique sibling for these short-lived ephemeral /
430                        // single-file embedded runtimes. The file-owned support-
431                        // dir tier (`File`) is already per-data unique, so leave
432                        // it to `for_destination` (#1375: the embedded audit then
433                        // still never lands a sibling next to the `.rdb`).
434                        let audit_path = reddb_file::layout::sibling_path(
435                            &data_path,
436                            &reddb_file::layout::sidecar_file_name(&data_path, "audit.log"),
437                        );
438                        Arc::new(crate::runtime::audit_log::AuditLogger::with_path(
439                            audit_path,
440                        ))
441                    } else {
442                        Arc::new(crate::runtime::audit_log::AuditLogger::for_destination(
443                            &audit_dest,
444                            &data_path,
445                        ))
446                    }
447                },
448                control_event_ledger: parking_lot::RwLock::new(Arc::new(
449                    crate::runtime::control_events::RuntimeLedger::new(db.store()),
450                )),
451                control_event_config: options.control_events,
452                query_audit: Arc::new(crate::runtime::query_audit::QueryAuditStream::new(
453                    db.store(),
454                    options.query_audit.clone(),
455                )),
456                lease_lifecycle: std::sync::OnceLock::new(),
457                routing_primary_endpoints: parking_lot::RwLock::new(Default::default()),
458                replica_apply_metrics: std::sync::Arc::new(
459                    crate::replication::logical::ReplicaApplyMetrics::default(),
460                ),
461                replica_link_metrics: std::sync::Arc::new(
462                    crate::replication::reconnect::ReplicaLinkMetrics::default(),
463                ),
464                quota_bucket: crate::runtime::quota_bucket::QuotaBucket::from_env(),
465                schema_vocabulary: parking_lot::RwLock::new(
466                    crate::runtime::schema_vocabulary::SchemaVocabulary::new(),
467                ),
468                slow_query_logger: {
469                    // Issue #205 — slow-query sink lives in the same
470                    // directory the audit log uses, so backup/restore
471                    // ships them together. Threshold + sample-pct
472                    // default conservatively (1 s, 100% sampling) so
473                    // emitted lines are rare and complete. Operators
474                    // tune via env / config matrix in a follow-up.
475                    //
476                    // gh-471 iter 2: same routing as the audit log —
477                    // `LogDestination::File(...)` for Performance/Max
478                    // lands under the file-owned support-directory logs tier;
479                    // lower tiers fall back to `red-slow.log` in the
480                    // data directory.
481                    // #1375 — see the audit-log note above: single-file mode
482                    // never writes the slow-query log as a sibling of the
483                    // `.rdb`. Route to a process-unique temp dir when embedded,
484                    // regardless of the data path.
485                    let fallback_dir = if embedded_single_file || options.read_only {
486                        std::env::temp_dir()
487                            .join("reddb-embedded-runtime")
488                            .join(format!("slow-{}", std::process::id()))
489                    } else {
490                        options
491                            .data_path
492                            .as_ref()
493                            .and_then(|p| p.parent().map(std::path::PathBuf::from))
494                            .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
495                    };
496                    let threshold_ms = std::env::var("RED_SLOW_QUERY_THRESHOLD_MS")
497                        .ok()
498                        .and_then(|s| s.parse::<u64>().ok())
499                        .unwrap_or(1000);
500                    let sample_pct = std::env::var("RED_SLOW_QUERY_SAMPLE_PCT")
501                        .ok()
502                        .and_then(|s| s.parse::<u8>().ok())
503                        .unwrap_or(100);
504                    let (_, slow_dest) = crate::api::tier_wiring::current_log_destinations();
505                    let slow_dest = if options.read_only {
506                        crate::storage::layout::LogDestination::Stderr
507                    } else {
508                        slow_dest
509                    };
510                    crate::telemetry::slow_query_logger::SlowQueryLogger::for_destination(
511                        &slow_dest,
512                        &fallback_dir,
513                        threshold_ms,
514                        sample_pct,
515                    )
516                },
517                slow_query_store: crate::telemetry::slow_query_store::SlowQueryStore::new(
518                    crate::telemetry::slow_query_store::DEFAULT_CAP,
519                ),
520                kv_stats: crate::runtime::KvStatsCounters::default(),
521                metrics_ingest_stats: crate::runtime::MetricsIngestCounters::default(),
522                metrics_tenant_activity_stats:
523                    crate::runtime::MetricsTenantActivityCounters::default(),
524                claim_telemetry: Arc::new(
525                    crate::runtime::claim_telemetry::ClaimTelemetryCounters::default(),
526                ),
527                queue_telemetry: Arc::new(
528                    crate::runtime::queue_telemetry::QueueTelemetryCounters::default(),
529                ),
530                query_latency_telemetry: Arc::new(
531                    crate::runtime::query_latency_telemetry::QueryLatencyTelemetry::default(),
532                ),
533                occupancy_sampler: Arc::new(
534                    crate::runtime::occupancy_sampler::OccupancySampler::new(),
535                ),
536                node_load_telemetry: Arc::new(
537                    crate::runtime::node_load_telemetry::NodeLoadTelemetry::default(),
538                ),
539                queue_presence: Arc::new(
540                    crate::storage::queue::presence::ConsumerPresenceRegistry::new(),
541                ),
542                vector_introspection: Arc::new(
543                    crate::storage::vector::introspection::VectorIntrospectionRegistry::new(),
544                ),
545                kv_tag_index: crate::runtime::KvTagIndex::default(),
546                chain_tip_cache: parking_lot::Mutex::new(HashMap::new()),
547                chain_integrity_broken: parking_lot::Mutex::new(HashMap::new()),
548                integrity_tombstones: parking_lot::Mutex::new(Vec::new()),
549                integrity_tombstones_state: std::sync::atomic::AtomicU8::new(0),
550            }),
551        };
552
553        runtime.install_hash_candidate_admission();
554
555        // Issue #205 — install the process-wide OperatorEvent sink so
556        // emit sites buried in storage / replication / signal handlers
557        // can record without threading an `&AuditLogger` through every
558        // call stack. First registration wins; subsequent in-memory
559        // runtimes (test harnesses) fall through to tracing+eprintln.
560        crate::telemetry::operator_event::install_global_audit_sink(Arc::clone(
561            &runtime.inner.audit_log,
562        ));
563
564        // Issue #1238 — wire the slow-query telemetry substrate (ADR 0060).
565        // The logger dual-writes: file sink (existing) + ring store (new).
566        runtime
567            .inner
568            .slow_query_logger
569            .attach_store(Arc::clone(&runtime.inner.slow_query_store));
570
571        // PLAN.md Phase 9.1 — backfill cold-start phase markers
572        // from the wall-clock captured before storage open. The
573        // entire `RedDB::open_with_options` call covers both
574        // auto-restore (when configured) and WAL replay. We
575        // record both phases against the same boundary today;
576        // a follow-up will split them once the storage layer
577        // surfaces a finer-grained event.
578        runtime
579            .inner
580            .lifecycle
581            .set_restore_started_at_ms(boot_open_start_ms);
582        runtime
583            .inner
584            .lifecycle
585            .set_restore_ready_at_ms(storage_ready_ms);
586        runtime
587            .inner
588            .lifecycle
589            .set_wal_replay_started_at_ms(boot_open_start_ms);
590        runtime
591            .inner
592            .lifecycle
593            .set_wal_replay_ready_at_ms(storage_ready_ms);
594
595        let restored_cdc_lsn = runtime
596            .inner
597            .db
598            .replication
599            .as_ref()
600            .map(|repl| {
601                repl.logical_wal_spool
602                    .as_ref()
603                    .map(|spool| spool.current_lsn())
604                    .unwrap_or(0)
605            })
606            .unwrap_or(0)
607            .max(runtime.config_u64("red.config.timeline.last_archived_lsn", 0));
608        runtime.inner.cdc.set_current_lsn(restored_cdc_lsn);
609        runtime.rehydrate_snapshot_xid_floor()?;
610        assert!(runtime
611            .inner
612            .db
613            .runtime
614            .set(super::WeakRuntime(Arc::downgrade(&runtime.inner)))
615            .is_ok());
616        assert!(
617            runtime
618                .inner
619                .db
620                .store()
621                .snapshot_manager
622                .set(runtime.snapshot_manager())
623                .is_ok(),
624            "a new runtime owns the store transaction outcomes"
625        );
626        runtime
627            .bootstrap_system_keyed_collections()
628            .map_err(|err| RedDBError::Internal(format!("bootstrap system collections: {err}")))?;
629        runtime.rehydrate_declared_column_schemas();
630        // Registry restoration can checkpoint missing required projections.
631        // Register their projectors before that epoch.
632        super::control_event_index::prepare(&runtime)?;
633        super::queue_storage_index::prepare(&runtime)?;
634        runtime.rehydrate_runtime_index_registry()?;
635        if runtime
636            .db()
637            .store()
638            .config()
639            .resident_memory_bytes_max
640            .is_some()
641        {
642            runtime
643                .db()
644                .store()
645                .checkpoint_missing_secondary_indices()?;
646        }
647        runtime.rehydrate_kv_tags()?;
648        runtime
649            .load_probabilistic_state()
650            .map_err(|err| RedDBError::Internal(format!("load probabilistic state: {err}")))?;
651
652        // Phase 2.5.4: replay `tenant_tables.{table}.column` markers so
653        // tables declared via `TENANT BY (col)` survive restart. Each
654        // entry re-registers the auto-policy and flips RLS on again.
655        runtime.rehydrate_tenant_tables();
656        // Issue #593 slice 9a — replay persisted materialized-view
657        // descriptors so `CREATE MATERIALIZED VIEW v AS …` survives a
658        // restart. Runs after the system-keyed collections bootstrap
659        // and before the API opens.
660        runtime.rehydrate_materialized_view_descriptors()?;
661        runtime.recover_store_forks()?;
662        if let Some(repl) = &runtime.inner.db.replication {
663            repl.wal_buffer.set_current_lsn(restored_cdc_lsn);
664        }
665
666        // Save system info to red_config on boot
667        {
668            let sys = SystemInfo::collect();
669            runtime.inner.db.store().set_config_tree(
670                "red.system",
671                &crate::serde_json::json!({
672                    "pid": sys.pid,
673                    "cpu_cores": sys.cpu_cores,
674                    "total_memory_bytes": sys.total_memory_bytes,
675                    "available_memory_bytes": sys.available_memory_bytes,
676                    "os": sys.os,
677                    "arch": sys.arch,
678                    "hostname": sys.hostname,
679                    "started_at": SystemTime::now()
680                        .duration_since(UNIX_EPOCH)
681                        .unwrap_or_default()
682                        .as_millis() as u64
683                }),
684            );
685
686            // Seed defaults on first boot (only if red_config is empty or missing defaults)
687            let store = runtime.inner.db.store();
688            if store
689                .get_collection("red_config")
690                .map(|m| m.query_all(|_| true).len())
691                .unwrap_or(0)
692                <= 10
693            {
694                store.set_config_tree("red.ai", &crate::json!({
695                    "default": crate::json!({
696                        "provider": "openai",
697                        "model": crate::ai::DEFAULT_OPENAI_PROMPT_MODEL
698                    }),
699                    "max_embedding_inputs": 256,
700                    "max_prompt_batch": 256,
701                    "timeout": crate::json!({ "connect_secs": 10, "read_secs": 90, "write_secs": 30 })
702                }));
703                store.set_config_tree(
704                    "red.server",
705                    &crate::json!({
706                        "max_scan_limit": 1000,
707                        "max_body_size": 1048576,
708                        "read_timeout_ms": 5000,
709                        "write_timeout_ms": 5000
710                    }),
711                );
712                store.set_config_tree(
713                    "red.storage",
714                    &crate::json!({
715                        "page_size": 4096,
716                        "page_cache_capacity": 100000,
717                        "auto_checkpoint_pages": 1000,
718                        "snapshot_retention": 16,
719                        "verify_checksums": true,
720                        "segment": crate::json!({
721                            "max_entities": 100000,
722                            "max_bytes": 268435456_u64,
723                            "compression_level": 6
724                        }),
725                        "hnsw": crate::json!({ "m": 16, "ef_construction": 100, "ef_search": 50 }),
726                        "ivf": crate::json!({ "n_lists": 100, "n_probes": 10 }),
727                        "bm25": crate::json!({ "k1": 1.2, "b": 0.75 })
728                    }),
729                );
730                store.set_config_tree(
731                    "red.search",
732                    &crate::json!({
733                        "rag": crate::json!({
734                            "max_chunks_per_source": 10,
735                            "max_total_chunks": 25,
736                            "similarity_threshold": 0.8,
737                            "graph_depth": 2,
738                            "min_relevance": 0.3
739                        }),
740                        "fusion": crate::json!({
741                            "vector_weight": 0.5,
742                            "graph_weight": 0.3,
743                            "table_weight": 0.2,
744                            "dedup_threshold": 0.85
745                        })
746                    }),
747                );
748                store.set_config_tree(
749                    "red.auth",
750                    &crate::json!({
751                        "enabled": false,
752                        "session_ttl_secs": 3600,
753                        "require_auth": false
754                    }),
755                );
756                store.set_config_tree(
757                    "red.query",
758                    &crate::json!({
759                        "connection_pool": crate::json!({ "max_connections": 64, "max_idle": 16 }),
760                        "max_recursion_depth": 1000
761                    }),
762                );
763                store.set_config_tree(
764                    "red.indexes",
765                    &crate::json!({
766                        "auto_select": true,
767                        "bloom_filter": crate::json!({
768                            "enabled": true,
769                            "false_positive_rate": 0.01,
770                            "prune_on_scan": true
771                        }),
772                        "hash": crate::json!({ "enabled": true }),
773                        "bitmap": crate::json!({ "enabled": true, "max_cardinality": 1000 }),
774                        "spatial": crate::json!({ "enabled": true })
775                    }),
776                );
777                store.set_config_tree(
778                    "red.probabilistic",
779                    &crate::json!({
780                        "hll_registers": 16384,
781                        "sketch_default_width": 1000,
782                        "sketch_default_depth": 5,
783                        "filter_default_capacity": 100000
784                    }),
785                );
786                store.set_config_tree(
787                    "red.timeseries",
788                    &crate::json!({
789                        "default_chunk_size": 1024,
790                        "compression": crate::json!({
791                            "timestamps": "delta_of_delta",
792                            "values": "gorilla_xor"
793                        }),
794                        "default_retention_days": 0
795                    }),
796                );
797                store.set_config_tree(
798                    "red.queue",
799                    &crate::json!({
800                        "default_max_size": 0,
801                        "default_max_attempts": 3,
802                        "visibility_timeout_ms": 30000,
803                        "consumer_idle_timeout_ms": 60000
804                    }),
805                );
806                store.set_config_tree(
807                    "red.backup",
808                    &crate::json!({
809                        "enabled": false,
810                        "interval_secs": 3600,
811                        "retention_count": 24,
812                        "upload": false,
813                        "backend": "local"
814                    }),
815                );
816                store.set_config_tree(
817                    "red.wal",
818                    &crate::json!({
819                        "archive": crate::json!({
820                            "enabled": false,
821                            "retention_hours": 168,
822                            "prefix": reddb_file::backup_wal_prefix("")
823                        })
824                    }),
825                );
826                store.set_config_tree(
827                    "red.cdc",
828                    &crate::json!({
829                        "enabled": true,
830                        "buffer_size": 100000
831                    }),
832                );
833                store.set_config_tree(
834                    "red.config.secret",
835                    &crate::json!({
836                        "auto_encrypt": true,
837                        "auto_decrypt": true
838                    }),
839                );
840            }
841
842            // Perf-parity config matrix: heal the Tier A (critical)
843            // keys unconditionally on every boot. Idempotent — only
844            // writes the default when the key is missing. Keeps
845            // `SHOW CONFIG` showing every guarantee the operator has
846            // (durability.mode, concurrency.locking.enabled, …) even
847            // on long-running datadirs that predate the matrix.
848            crate::runtime::config_matrix::heal_critical_keys(store.as_ref());
849            seed_storage_deploy_config(store.as_ref(), options.storage_profile);
850
851            // Phase 5 — Lehman-Yao runtime flag. Read the Tier A
852            // `storage.btree.lehman_yao` value from the matrix (env
853            // > file > red_config > default) and publish it to the
854            // storage layer's atomic so the B-tree read / split
855            // paths can branch without re-reading the config on
856            // every hot-path call.
857            let lehman_yao = runtime.config_bool("storage.btree.lehman_yao", true);
858            crate::storage::engine::btree::lehman_yao::set_enabled(lehman_yao);
859            if lehman_yao {
860                tracing::info!(
861                    "storage.btree.lehman_yao=true — lock-free concurrent descent enabled"
862                );
863            }
864
865            // Config file overlay — mounted `/etc/reddb/config.json`
866            // (override path via REDDB_CONFIG_FILE). Writes keys with
867            // write-if-absent semantics so a later user `SET CONFIG`
868            // always wins. Missing file = silent no-op.
869            let overlay_path = crate::runtime::config_overlay::config_file_path();
870            let _ =
871                crate::runtime::config_overlay::apply_config_file(store.as_ref(), &overlay_path);
872        }
873
874        // VCS ("Git for Data") — create the `red_*` metadata
875        // collections on first boot. Idempotent: `get_or_create_collection`
876        // is a no-op if the collection already exists.
877        {
878            let store = runtime.inner.db.store();
879            for name in crate::application::vcs_collections::ALL {
880                let _ = store.get_or_create_collection(*name);
881            }
882            // Seed VCS config namespace with sensible defaults on first
883            // boot, matching the pattern used by red.ai / red.storage.
884            store.set_config_tree(
885                crate::application::vcs_collections::CONFIG_NAMESPACE,
886                &crate::json!({
887                    "default_branch": "main",
888                    "author": crate::json!({
889                        "name": "reddb",
890                        "email": "reddb@localhost"
891                    }),
892                    "protected_branches": crate::json!(["main"]),
893                    "closure": crate::json!({
894                        "enabled": true,
895                        "lazy": true
896                    }),
897                    "merge": crate::json!({
898                        "default_strategy": "auto",
899                        "fast_forward": true
900                    })
901                }),
902            );
903        }
904
905        // Migrations — create the `red_migrations` / `red_migration_deps`
906        // system collections on first boot. Idempotent.
907        {
908            let store = runtime.inner.db.store();
909            for name in crate::application::migration_collections::ALL {
910                let _ = store.get_or_create_collection(*name);
911            }
912        }
913
914        // Topology graph (#803) — ensure the built-in `red.topology.cluster`
915        // graph collection (declared WITH ANALYTICS) and its metadata sidecar
916        // exist. Idempotent and survives restarts via the WAL-backed contract.
917        let _ = crate::application::topology_collections::ensure(&runtime);
918
919        // Bootstrapping topology can publish a metadata checkpoint. Register
920        // the optional COUNT projection only after that publication, so old
921        // data is not backfilled before readiness. Queries scan until the next
922        // checkpoint publishes the projection.
923        runtime.prepare_paged_counts()?;
924
925        // #1369 — reserve a fixed internal-id floor so the first user-inserted
926        // entity always receives a stable, documented `rid` (FIRST_USER_ENTITY_ID),
927        // independent of how many internal collection-descriptor / config-default
928        // entities the boot sequence seeded above. `register_entity_id` only ever
929        // raises the allocator, so a database that already holds user data
930        // (counter past the floor) is untouched; a freshly-seeded database jumps
931        // straight to the floor.
932        runtime
933            .inner
934            .db
935            .store()
936            .register_entity_id(crate::storage::EntityId::new(
937                crate::storage::FIRST_USER_ENTITY_ID - 1,
938            ));
939
940        // Start background maintenance thread (context index refresh +
941        // session purge). Held by a WEAK reference to `RuntimeInner`
942        // to avoid an ownership cycle. The last foreground owner unparks
943        // and joins the worker before releasing its inner reference, so
944        // an active weak upgrade cannot retain the pager lock after close.
945        // The 60-second work cadence is tracked via `last_work`.
946        //
947        // The previous version captured `rt = runtime.clone()` by
948        // strong reference and ran an unterminated `loop`, which held
949        // Arc<RuntimeInner> forever — reopening a persistent database
950        // in the same process failed with "Database is locked" because
951        // the pager could never drop. See the regression test
952        // `finding_1_select_after_bulk_insert_persistent_reopen`.
953        {
954            let weak = Arc::downgrade(&runtime.inner);
955            runtime
956                .background_tasks
957                .as_ref()
958                .expect("foreground runtime")
959                .spawn(
960                    std::thread::Builder::new().name("reddb-maintenance".into()),
961                    move |stopped| {
962                        let tick = std::time::Duration::from_millis(200);
963                        let work_interval = std::time::Duration::from_secs(60);
964                        let mut last_work = std::time::Instant::now();
965                        let mut allocator_pressure =
966                            super::allocator_pressure::AllocatorPressure::default();
967                        loop {
968                            std::thread::park_timeout(tick);
969                            if stopped.load(AtomicOrdering::Acquire) {
970                                break;
971                            }
972                            let Some(inner) = weak.upgrade() else {
973                                // All strong references dropped — the
974                                // runtime is gone, exit cleanly.
975                                break;
976                            };
977                            if last_work.elapsed() >= work_interval {
978                                let _stats = inner.db.store().context_index().stats();
979                                last_work = std::time::Instant::now();
980                            }
981                            let rt = RedDBRuntime {
982                                background_tasks: None,
983                                inner,
984                            };
985                            rt.try_reclaim_history_tick();
986                            allocator_pressure.tick();
987                        }
988                    },
989                );
990        }
991
992        // Start backup scheduler if enabled via red_config
993        {
994            let store = runtime.inner.db.store();
995            let mut backup_enabled = false;
996            let mut backup_interval = 3600u64;
997
998            if let Some(manager) = store.get_collection("red_config") {
999                manager.for_each_entity(|entity| {
1000                    if let Some(row) = entity.data.as_row() {
1001                        let key = row.get_field("key").and_then(|v| match v {
1002                            reddb_types::Value::Text(s) => Some(s.as_ref()),
1003                            _ => None,
1004                        });
1005                        let val = row.get_field("value");
1006                        if key == Some("red.config.backup.enabled") {
1007                            backup_enabled = match val {
1008                                Some(reddb_types::Value::Boolean(true)) => true,
1009                                Some(reddb_types::Value::Text(s)) => &**s == "true",
1010                                _ => false,
1011                            };
1012                        } else if key == Some("red.config.backup.interval_secs") {
1013                            if let Some(reddb_types::Value::Integer(n)) = val {
1014                                backup_interval = *n as u64;
1015                            }
1016                        }
1017                    }
1018                    true
1019                });
1020            }
1021
1022            if backup_enabled {
1023                runtime.inner.backup_scheduler.set_interval(backup_interval);
1024                let rt = runtime.clone();
1025                runtime
1026                    .inner
1027                    .backup_scheduler
1028                    .start(move || rt.trigger_backup().map_err(|e| format!("{}", e)));
1029            }
1030        }
1031
1032        // Load EC registry from red_config and start worker
1033        {
1034            runtime
1035                .inner
1036                .ec_registry
1037                .load_from_config_store(runtime.inner.db.store().as_ref());
1038            if !runtime.inner.db.options().read_only
1039                && !runtime.inner.ec_registry.async_configs().is_empty()
1040            {
1041                runtime.inner.ec_worker.start(
1042                    Arc::clone(&runtime.inner.ec_registry),
1043                    Arc::clone(&runtime.inner.db.store()),
1044                );
1045            }
1046        }
1047
1048        if let (true, crate::replication::ReplicationRole::Replica { primary_addr }) = (
1049            runtime.inner.db.options().replica_loop_enabled
1050                && !runtime.inner.db.options().read_only,
1051            runtime.inner.db.options().replication.role.clone(),
1052        ) {
1053            let rt = runtime.clone();
1054            std::thread::Builder::new()
1055                .name("reddb-replica".into())
1056                .spawn(move || rt.run_replica_loop(primary_addr))
1057                .ok();
1058        }
1059
1060        // PLAN.md Phase 1 — Lifecycle Contract. Mark Ready once every
1061        // boot stage above has completed (WAL replay, restore-from-
1062        // remote, replica-loop spawn). Health probes flip from 503 to
1063        // 200 here; shutdown begins from this state.
1064        runtime.inner.lifecycle.mark_ready();
1065
1066        #[cfg(not(test))]
1067        if runtime
1068            .db()
1069            .store()
1070            .config()
1071            .resident_memory_bytes_max
1072            .is_some()
1073            && !runtime.db().options().read_only
1074        {
1075            runtime.start_count_projection_worker();
1076        }
1077
1078        // Issue #583 slice 10 — ContinuousMaterializedView scheduler.
1079        // Low-priority background ticker that drains the cache's
1080        // `claim_due_at` set every ~50ms. Holds only a Weak<RuntimeInner>
1081        // to avoid an ownership cycle; foreground close unparks and drains
1082        // an active refresh before releasing storage. Materialized views without
1083        // a `REFRESH EVERY` clause stay on the manual-refresh path
1084        // and are skipped by `claim_due_at`, so the loop is a no-op
1085        // when no scheduled views exist.
1086        {
1087            let weak_inner = Arc::downgrade(&runtime.inner);
1088            runtime
1089                .background_tasks
1090                .as_ref()
1091                .expect("foreground runtime")
1092                .spawn(
1093                    std::thread::Builder::new()
1094                        .name("reddb-mv-scheduler".into())
1095                        // Rust's default for spawned threads is 2 MiB, which is too
1096                        // small for the query-execution path that refresh_due_materialized_views
1097                        // runs (StatementExecutionFrame + guards per view). Match the
1098                        // 8 MiB that the OS gives the main thread.
1099                        .stack_size(8 * 1024 * 1024),
1100                    move |stopped| loop {
1101                        std::thread::park_timeout(std::time::Duration::from_millis(50));
1102                        if stopped.load(AtomicOrdering::Acquire) {
1103                            break;
1104                        }
1105                        let Some(inner) = weak_inner.upgrade() else {
1106                            break;
1107                        };
1108                        let rt = RedDBRuntime {
1109                            background_tasks: None,
1110                            inner,
1111                        };
1112                        rt.refresh_due_materialized_views();
1113                    },
1114                );
1115        }
1116
1117        // Issue #584 slice 12 — DeclarativeRetention background sweeper.
1118        // Low-priority ticker that physically reclaims rows whose
1119        // timestamp has fallen beyond the retention window. Holds a
1120        // `Weak<RuntimeInner>`; foreground close drains an active tick
1121        // before releasing storage (shutdown leaves storage consistent
1122        // because each tick goes through the standard DELETE path —
1123        // there is no half-finished mutation state to clean up). The
1124        // tick interval is intentionally longer than the MV scheduler
1125        // (500ms) because retention is order-of-seconds at minimum.
1126        if !runtime.write_gate().is_read_only() {
1127            let weak_inner = Arc::downgrade(&runtime.inner);
1128            runtime
1129                .background_tasks
1130                .as_ref()
1131                .expect("foreground runtime")
1132                .spawn(
1133                    std::thread::Builder::new().name("reddb-retention-sweeper".into()),
1134                    move |stopped| loop {
1135                        std::thread::park_timeout(std::time::Duration::from_millis(500));
1136                        if stopped.load(AtomicOrdering::Acquire) {
1137                            break;
1138                        }
1139                        let Some(inner) = weak_inner.upgrade() else {
1140                            break;
1141                        };
1142                        let rt = RedDBRuntime {
1143                            background_tasks: None,
1144                            inner,
1145                        };
1146                        rt.sweep_retention_tick(
1147                            crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
1148                        );
1149                        rt.sweep_individual_ttl_tick(
1150                            crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
1151                        );
1152                    },
1153                );
1154        }
1155
1156        // Anonymous access is the most consequential posture an operator can
1157        // choose. It was only ever announced on stderr; leave it in the
1158        // chained ledger too, so an audit of the store itself shows it.
1159        if no_auth_boot {
1160            use crate::runtime::control_events::{
1161                ActorRef, ControlEvent, ControlEventCtx, EventKind, Outcome,
1162            };
1163            let ledger = std::sync::Arc::clone(&runtime.inner.control_event_ledger.read());
1164            let ctx = ControlEventCtx {
1165                actor: ActorRef::System("boot"),
1166                scope: None,
1167                request_id: None,
1168                trace_id: None,
1169            };
1170            let event = ControlEvent {
1171                kind: EventKind::AuthDisabled,
1172                outcome: Outcome::Allowed,
1173                action: std::borrow::Cow::Borrowed("auth.disabled"),
1174                resource: Some("server".to_string()),
1175                reason: Some("--no-auth / --dev".to_string()),
1176                matched_policy_id: None,
1177                fields: std::collections::HashMap::new(),
1178            };
1179            if let Err(err) = ledger.emit(&ctx, event) {
1180                tracing::warn!(
1181                    target: "reddb::security",
1182                    error = %err,
1183                    "could not record the auth-disabled boot in the control-event ledger"
1184                );
1185            }
1186        }
1187
1188        Ok(runtime)
1189    }
1190
1191    fn rehydrate_snapshot_xid_floor(&self) -> RedDBResult<()> {
1192        let store = self.inner.db.store();
1193        let snapshot_manager = self.inner.transaction_state.snapshot_manager();
1194        let base_high_water_mark = store.startup_xid_high_water_mark();
1195        if let Some(high_water_mark) = base_high_water_mark {
1196            snapshot_manager.observe_committed_xid(high_water_mark);
1197        }
1198        for collection in store.list_collections() {
1199            let Some(manager) = store.get_collection(&collection) else {
1200                continue;
1201            };
1202            let observe = |entity: &crate::storage::UnifiedEntity| {
1203                snapshot_manager.observe_committed_xid(entity.xmin);
1204                snapshot_manager.observe_committed_xid(entity.xmax);
1205                true
1206            };
1207            if base_high_water_mark.is_some() {
1208                // The catalog covers the exact immutable roots. Committed WAL
1209                // replay remains resident and must still advance the floor.
1210                manager.for_each_entity(observe);
1211            } else {
1212                // Older or unproven generations require complete physical
1213                // validation, one bounded record at a time, before filtering.
1214                manager.try_for_each_entity(usize::MAX, observe)?;
1215            }
1216        }
1217        if base_high_water_mark.is_none() {
1218            store.remember_startup_xid_high_water_mark(
1219                snapshot_manager.peek_next_xid().saturating_sub(1),
1220            );
1221        }
1222        Ok(())
1223    }
1224
1225    /// Provision an empty Table-shaped collection that backs a
1226    /// `CREATE MATERIALIZED VIEW v` (issue #594 slice 9b of #575).
1227    /// `SELECT FROM v` reads this collection directly; the rewriter is
1228    /// configured to skip materialized views so the body is no longer
1229    /// substituted. REFRESH still writes to the cache slot — wiring it
1230    /// into this backing collection is the job of slice 9c.
1231    ///
1232    /// Idempotent: re-running for the same name leaves the existing
1233    /// collection in place (mirrors `CREATE TABLE IF NOT EXISTS`
1234    /// semantics). This keeps `CREATE OR REPLACE MATERIALIZED VIEW v`
1235    /// cheap — the body change does not invalidate already-buffered
1236    /// rows. Until 9c lands the backing is always empty anyway.
1237    pub(crate) fn ensure_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
1238        let store = self.inner.db.store();
1239        let mut changed = false;
1240        if store.get_collection(name).is_none() {
1241            store.get_or_create_collection(name);
1242            changed = true;
1243        }
1244        if self.inner.db.collection_contract(name).is_none() {
1245            self.inner
1246                .db
1247                .save_collection_contract(system_keyed_collection_contract(
1248                    name,
1249                    crate::catalog::CollectionModel::Table,
1250                ))
1251                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1252            changed = true;
1253        }
1254        if changed {
1255            self.inner
1256                .db
1257                .persist_metadata()
1258                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1259        }
1260        Ok(())
1261    }
1262
1263    /// Inverse of [`ensure_materialized_view_backing`] — drops the
1264    /// backing collection on `DROP MATERIALIZED VIEW v`. No-op when
1265    /// the collection was never created (e.g. a `DROP MATERIALIZED
1266    /// VIEW IF EXISTS v` against an unknown name).
1267    pub(crate) fn drop_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
1268        let store = self.inner.db.store();
1269        if store.get_collection(name).is_none() {
1270            return Ok(());
1271        }
1272        store
1273            .drop_collection(name)
1274            .map_err(|err| RedDBError::Internal(err.to_string()))?;
1275        // The contract may have been dropped already (DROP TABLE path)
1276        // — ignore "not found" errors by checking presence first.
1277        if self.inner.db.collection_contract(name).is_some() {
1278            self.inner
1279                .db
1280                .remove_collection_contract(name)
1281                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1282        }
1283        self.invalidate_result_cache();
1284        self.inner
1285            .db
1286            .persist_metadata()
1287            .map_err(|err| RedDBError::Internal(err.to_string()))?;
1288        Ok(())
1289    }
1290
1291    fn bootstrap_system_keyed_collections(&self) -> RedDBResult<()> {
1292        let mut changed = false;
1293        for (name, model) in [
1294            ("red.config", crate::catalog::CollectionModel::Config),
1295            ("red.vault", crate::catalog::CollectionModel::Vault),
1296            // Issue #593 — materialized-view catalog. One row per
1297            // `CREATE MATERIALIZED VIEW`; rehydrated at boot before
1298            // the API opens.
1299            (
1300                crate::runtime::continuous_materialized_view::CATALOG_COLLECTION,
1301                crate::catalog::CollectionModel::Config,
1302            ),
1303        ] {
1304            if self.inner.db.store().get_collection(name).is_none() {
1305                self.inner.db.store().get_or_create_collection(name);
1306                changed = true;
1307            }
1308            if self.inner.db.collection_contract(name).is_none() {
1309                let contract = system_keyed_collection_contract(name, model);
1310                if self.inner.db.options().read_only {
1311                    self.inner.db.cache_read_only_bootstrap_contract(contract);
1312                } else {
1313                    self.inner
1314                        .db
1315                        .save_collection_contract(contract)
1316                        .map_err(|err| RedDBError::Internal(err.to_string()))?;
1317                }
1318                changed = true;
1319            }
1320        }
1321        if changed {
1322            self.inner
1323                .db
1324                .persist_metadata()
1325                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1326        }
1327        Ok(())
1328    }
1329
1330    pub fn db(&self) -> Arc<RedDB> {
1331        Arc::clone(&self.inner.db)
1332    }
1333
1334    /// Direct access to the runtime's secondary-index store.
1335    /// Used by bulk-insert entry points (gRPC binary bulk, HTTP bulk,
1336    /// wire bulk) that need to push new rows through the per-index
1337    /// maintenance hook after `store.bulk_insert` returns.
1338    pub fn index_store_ref(&self) -> &super::index_store::IndexStore {
1339        &self.inner.index_store
1340    }
1341
1342    /// Apply a DDL event to the schema-vocabulary reverse index
1343    /// (issue #120). Called by DDL execution paths after the catalog
1344    /// mutation has succeeded so the index never holds entries for
1345    /// half-applied DDL.
1346    pub(crate) fn schema_vocabulary_apply(
1347        &self,
1348        event: crate::runtime::schema_vocabulary::DdlEvent,
1349    ) {
1350        self.inner.schema_vocabulary.write().on_ddl(event);
1351    }
1352
1353    /// Lookup `token` in the schema-vocabulary reverse index. Returns
1354    /// an owned `Vec<VocabHit>` because the underlying read lock
1355    /// cannot be borrowed across the call boundary; the slice from
1356    /// `SchemaVocabulary::lookup` is cloned per hit.
1357    pub fn schema_vocabulary_lookup(
1358        &self,
1359        token: &str,
1360    ) -> Vec<crate::runtime::schema_vocabulary::VocabHit> {
1361        self.inner.schema_vocabulary.read().lookup(token).to_vec()
1362    }
1363
1364    /// Inject an AuthStore into the runtime. Called by server boot
1365    /// after the vault has been bootstrapped, so that `Value::Secret`
1366    /// auto-encrypt/decrypt can reach the vault AES key.
1367    pub fn set_auth_store(&self, store: Arc<crate::auth::store::AuthStore>) {
1368        *self.inner.auth_store.write() = Some(store);
1369    }
1370
1371    /// Snapshot the current AuthStore (if any). Used by the wire listener
1372    /// to validate bearer tokens issued via HTTP `/auth/login`.
1373    pub fn auth_store(&self) -> Option<Arc<crate::auth::store::AuthStore>> {
1374        self.inner.auth_store.read().clone()
1375    }
1376
1377    /// Inject an `OAuthValidator` into the runtime. When set, HTTP and
1378    /// wire transports try OAuth JWT validation before falling back to
1379    /// the local AuthStore lookup. Pass `None` to disable.
1380    pub fn set_oauth_validator(&self, validator: Option<Arc<crate::auth::oauth::OAuthValidator>>) {
1381        *self.inner.oauth_validator.write() = validator;
1382    }
1383
1384    /// Returns a clone of the configured `OAuthValidator` Arc, if any.
1385    /// Hot path: called per HTTP request when an Authorization header
1386    /// is present, so we hand back a cheap Arc clone.
1387    pub fn oauth_validator(&self) -> Option<Arc<crate::auth::oauth::OAuthValidator>> {
1388        self.inner.oauth_validator.read().clone()
1389    }
1390
1391    /// Inject the browser-token authority (issue #936). When set, the
1392    /// RedWire WS handshake accepts the short-lived access JWT it mints
1393    /// (alongside, and tried before, the federated OAuth validator), and
1394    /// the `/auth/browser/*` HTTP endpoints can issue/rotate the pair.
1395    /// `None` leaves the browser credential flow inert.
1396    pub fn set_browser_token_authority(
1397        &self,
1398        authority: Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>>,
1399    ) {
1400        *self.inner.browser_token_authority.write() = authority;
1401    }
1402
1403    /// Snapshot the browser-token authority, if wired. Read on the WS
1404    /// handshake path and by the `/auth/browser/*` handlers; a cheap Arc
1405    /// clone keeps the lock hold short.
1406    pub fn browser_token_authority(
1407        &self,
1408    ) -> Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>> {
1409        self.inner.browser_token_authority.read().clone()
1410    }
1411}
1412
1413#[cfg(test)]
1414mod xid_resume_tests {
1415    use super::*;
1416    use reddb_engine::storage::collection_read::{CollectionCursor, CollectionRead, EncodedRecord};
1417
1418    struct Unreadable;
1419
1420    fn unavailable() -> crate::storage::StoreError {
1421        crate::storage::StoreError::Io(std::io::Error::other("XID bootstrap read a base payload"))
1422    }
1423
1424    impl CollectionRead for Unreadable {
1425        fn get_record(
1426            &self,
1427            id: crate::storage::EntityId,
1428            _: usize,
1429        ) -> Result<Option<EncodedRecord>, crate::storage::StoreError> {
1430            if id.raw() == 999 {
1431                Ok(None)
1432            } else {
1433                Err(unavailable())
1434            }
1435        }
1436        fn cursor(&self) -> Result<Box<dyn CollectionCursor + '_>, crate::storage::StoreError> {
1437            Err(unavailable())
1438        }
1439        fn owned_cursor(&self) -> Result<Box<dyn CollectionCursor>, crate::storage::StoreError> {
1440            Err(unavailable())
1441        }
1442    }
1443
1444    #[test]
1445    fn xid_resume_survives_pending_checkpoint_process_exit() {
1446        const FIXTURE: &str = "REDDB_XID_RESUME_CRASH_FIXTURE";
1447        let open = |path: std::path::PathBuf| {
1448            let options = RedDBOptions::persistent(path)
1449                .with_memory_budget(64 * 1024 * 1024)
1450                .with_storage_profile(
1451                    crate::storage::profile::StorageDeployPreset::PrimaryReplicaDev.selection(),
1452                )
1453                .expect("profile");
1454            crate::storage::RedDB::with_disk_residency_for_qualification(|| {
1455                RedDBRuntime::with_options(options)
1456            })
1457            .expect("crash recovery runtime")
1458        };
1459        if let Some(root) = std::env::var_os(FIXTURE) {
1460            let root = std::path::PathBuf::from(root);
1461            assert_eq!(
1462                std::fs::read_to_string(root.join("owned-fixture")).expect("owned fixture"),
1463                "xid-resume-crash-v1"
1464            );
1465            let runtime = open(root.join("crash.rdb"));
1466            for sql in [
1467                "CREATE TABLE crash_rows (id INTEGER, payload TEXT)",
1468                "INSERT INTO crash_rows (id, payload) VALUES (1, 'committed')",
1469                "BEGIN",
1470                "UPDATE crash_rows SET payload = 'pending' WHERE id = 1",
1471                "INSERT INTO crash_rows (id, payload) VALUES (2, 'pending')",
1472            ] {
1473                runtime.execute_query(sql).expect("crash fixture statement");
1474            }
1475            runtime
1476                .flush()
1477                .expect("checkpoint with pending transaction");
1478            std::fs::write(
1479                root.join("checkpoint-complete"),
1480                "pending transaction remains open",
1481            )
1482            .expect("acknowledged fixture checkpoint");
1483            // Deliberately skip all runtime destructors and rollback. The
1484            // parent must recover the committed preimage from the native file.
1485            std::process::exit(0);
1486        }
1487        let directory = tempfile::tempdir().expect("owned crash fixture");
1488        std::fs::write(
1489            directory.path().join("owned-fixture"),
1490            "xid-resume-crash-v1",
1491        )
1492        .expect("ownership marker");
1493        let child = std::process::Command::new(std::env::current_exe().expect("test executable"))
1494            .args([
1495                "--exact",
1496                "runtime::impl_lifecycle::xid_resume_tests::xid_resume_survives_pending_checkpoint_process_exit",
1497                "--test-threads=1",
1498            ])
1499            .env(FIXTURE, directory.path())
1500            .output()
1501            .expect("crashing owned child");
1502        assert!(
1503            child.status.success(),
1504            "{}",
1505            String::from_utf8_lossy(&child.stderr)
1506        );
1507        assert!(directory.path().join("checkpoint-complete").is_file());
1508        let path = directory.path().join("crash.rdb");
1509        assert!(path.is_file());
1510        let runtime = open(path.clone());
1511        let result = runtime
1512            .execute_query("SELECT id, payload FROM crash_rows ORDER BY id")
1513            .expect("committed preimage");
1514        assert_eq!(
1515            result.result.records.len(),
1516            1,
1517            "pending insert must stay invisible"
1518        );
1519        assert_eq!(result.result.records[0].get("id"), Some(&Value::Integer(1)));
1520        assert_eq!(
1521            result.result.records[0].get("payload"),
1522            Some(&Value::text("committed"))
1523        );
1524        runtime
1525            .execute_query("INSERT INTO crash_rows (id, payload) VALUES (3, 'after')")
1526            .expect("new writer after recovery");
1527        runtime.flush().expect("recovered checkpoint");
1528        drop(runtime);
1529        let reopened = open(path);
1530        let result = reopened
1531            .execute_query("SELECT id FROM crash_rows ORDER BY id")
1532            .expect("second recovery");
1533        assert_eq!(result.result.records.len(), 2);
1534        assert_eq!(result.result.records[0].get("id"), Some(&Value::Integer(1)));
1535        assert_eq!(result.result.records[1].get("id"), Some(&Value::Integer(3)));
1536    }
1537
1538    #[test]
1539    fn xid_resume_skips_known_base_payloads_but_rejects_an_unproven_root() {
1540        let directory = tempfile::tempdir().expect("resume fixture");
1541        let options = RedDBOptions::persistent(directory.path().join("resume.rdb"))
1542            .with_memory_budget(64 * 1024 * 1024)
1543            .with_storage_profile(
1544                crate::storage::profile::StorageDeployPreset::PrimaryReplicaDev.selection(),
1545            )
1546            .expect("profile");
1547        let runtime = crate::storage::RedDB::with_disk_residency_for_qualification(|| {
1548            RedDBRuntime::with_options(options)
1549        })
1550        .expect("runtime");
1551        runtime
1552            .execute_query("CREATE TABLE resume_rows (id INTEGER)")
1553            .expect("table");
1554        runtime
1555            .execute_query("INSERT INTO resume_rows (id) VALUES (1)")
1556            .expect("row");
1557        runtime.flush().expect("publish complete XID summary");
1558        let store = runtime.db().store();
1559        assert!(store.startup_xid_high_water_mark().is_some());
1560        let root = store
1561            .collection_root_page("resume_rows")
1562            .expect("published root");
1563        let manager = store.get_collection("resume_rows").expect("manager");
1564        for (candidate, proven) in [(root, true), (root + 1, false)] {
1565            manager.install_backing(
1566                Arc::new(Unreadable),
1567                candidate,
1568                store.format_version(),
1569                4096,
1570                1,
1571            );
1572            let mut overlay = crate::storage::UnifiedEntity::table_row(
1573                crate::storage::EntityId::new(999),
1574                "resume_rows",
1575                999,
1576                vec![Value::Integer(2)],
1577            );
1578            overlay.xmin = 500_000;
1579            overlay.xmax = 600_000;
1580            manager.insert(overlay).expect("resident WAL overlay");
1581            let result = runtime.rehydrate_snapshot_xid_floor();
1582            assert_eq!(result.is_ok(), proven, "root={candidate}: {result:?}");
1583            if proven {
1584                assert!(runtime.snapshot_manager().peek_next_xid() > 600_000);
1585            }
1586        }
1587    }
1588}