Skip to main content

velesdb_core/database/
mod.rs

1//! Database facade and orchestration layer for collection lifecycle and query routing.
2//!
3//! This module is split into focused submodules:
4//!
5//! - [`collection_ops`] — Collection CRUD dispatcher (create, delete, list, get)
6//! - [`vector_ops`] — Vector collection create/get
7//! - [`graph_ops`] — Graph collection create/get
8//! - [`metadata_ops`] — Metadata-only collection create/get
9//! - [`query_engine`] — `VelesQL` query execution, plan caching, DML dispatch
10//! - [`query_join`] — JOIN execution strategies (lookup, filtered, condition pushdown)
11//! - [`dml_executor`] — DML mutations (INSERT EDGE, DELETE, DELETE EDGE, SELECT EDGES, INSERT NODE)
12//! - [`persistence`] — Loading collections from disk at startup
13//! - [`training`] — `TRAIN QUANTIZER` statement execution
14//! - [`stats`] — Collection statistics (analyze, cache)
15//! - [`database_helpers`] — DML value conversion and JOIN column store helpers
16
17use crate::collection::{GraphCollection, MetadataCollection, VectorCollection};
18use crate::observer::DatabaseObserver;
19use crate::simd_dispatch;
20use crate::{ColumnStore, Error, Result};
21
22/// Shared out-cell for the executed filter strategy, threaded from the
23/// EXPLAIN ANALYZE counted path down to the base collection's query pipeline.
24pub(crate) type StrategyProbeSlot = std::sync::Arc<crate::guardrails::ExecutedStrategyCell>;
25
26mod admin_executor;
27mod collection_ops;
28mod cross_collection;
29mod ddl_executor;
30mod dml_executor;
31mod gated_search;
32mod graph_ops;
33mod introspection_executor;
34mod join_pushdown;
35mod metadata_ops;
36mod persistence;
37mod query_engine;
38mod query_engine_agg;
39mod query_engine_dml;
40mod query_join;
41mod stats;
42mod subquery_resolver;
43mod training;
44mod vector_ops;
45
46#[cfg(feature = "persistence")]
47mod database_helpers;
48
49#[cfg(all(test, feature = "persistence"))]
50mod collection_ops_tests;
51#[cfg(all(test, feature = "persistence"))]
52mod database_helpers_tests;
53#[cfg(all(test, feature = "persistence"))]
54mod database_tests;
55#[cfg(all(test, feature = "persistence"))]
56mod ddl_executor_tests;
57#[cfg(all(test, feature = "persistence"))]
58mod graph_ops_tests;
59#[cfg(all(test, feature = "persistence"))]
60mod hnsw_config_wiring_tests;
61#[cfg(all(test, feature = "persistence"))]
62mod query_engine_tests;
63#[cfg(all(test, feature = "persistence"))]
64mod query_join_tests;
65#[cfg(all(test, feature = "persistence"))]
66mod stats_tests;
67
68pub use gated_search::GatedRead;
69
70/// Database instance managing collections and storage.
71///
72/// # Lifecycle
73///
74/// `Database::open()` automatically loads all previously created collections from disk.
75/// There is no need to call `load_collections()` separately.
76///
77/// # Extension (Premium)
78///
79/// Use [`Database::open_with_observer`] to inject a [`DatabaseObserver`] implementation
80/// from `velesdb-premium` without modifying this crate.
81#[cfg(feature = "persistence")]
82pub struct Database {
83    /// Path to the data directory
84    data_dir: std::path::PathBuf,
85    /// Exclusive file lock preventing multi-process corruption.
86    ///
87    /// The lock is held for the lifetime of the `Database` and released on `Drop`.
88    /// The `_` prefix signals this field is kept for its RAII side effect.
89    _lock_file: std::fs::File,
90    /// Root configuration applied to every subsystem.
91    ///
92    /// Stored as an `Arc` so `Database::config()` can hand out cheap,
93    /// cloneable references without forcing the whole struct onto the
94    /// heap or locking. The value is populated at construction time
95    /// (`open`, `open_with_observer`, or `open_with_config`) and is
96    /// immutable for the life of the `Database` — Wave 3 never needs
97    /// to mutate the root config at runtime, and making it immutable
98    /// rules out a large class of surprising behaviours.
99    config: std::sync::Arc<crate::config::VelesConfig>,
100    /// Typed registry: vector collections.
101    vector_colls: parking_lot::RwLock<std::collections::HashMap<String, VectorCollection>>,
102    /// Typed registry: graph collections.
103    graph_colls: parking_lot::RwLock<std::collections::HashMap<String, GraphCollection>>,
104    /// Typed registry: metadata-only collections.
105    metadata_colls: parking_lot::RwLock<std::collections::HashMap<String, MetadataCollection>>,
106    /// Cached collection statistics for CBO planning.
107    collection_stats: parking_lot::RwLock<
108        std::collections::HashMap<String, crate::collection::stats::CollectionStats>,
109    >,
110    /// Optional lifecycle observer (used by velesdb-premium for RBAC, audit, multi-tenant).
111    observer: Option<std::sync::Arc<dyn DatabaseObserver>>,
112    /// Monotonic DDL schema version counter (CACHE-01).
113    ///
114    /// Incremented on every create/drop collection operation.
115    /// Used by `CompiledPlanCache` to invalidate cached query plans.
116    schema_version: std::sync::atomic::AtomicU64,
117    /// Compiled query plan cache (CACHE-02).
118    ///
119    /// Stores recently compiled `QueryPlan` instances keyed by `PlanKey`.
120    /// Default sizing: L1 = 1K hot entries, L2 = 10K LRU entries.
121    compiled_plan_cache: crate::cache::CompiledPlanCache,
122    /// JOIN-side `ColumnStore` cache keyed by collection name (CACHE-03).
123    ///
124    /// An entry is valid only while its `(schema_version, write_generation)`
125    /// stamp matches the live counters, so any mutation — or a drop/recreate
126    /// under the same name — forces a rebuild. Entries are also purged
127    /// eagerly on `delete_collection`. Collections carrying TTL points are
128    /// never cached (expiry does not bump `write_generation`).
129    join_store_cache:
130        parking_lot::RwLock<std::collections::HashMap<String, query_join::JoinStoreEntry>>,
131}
132
133#[cfg(feature = "persistence")]
134impl Database {
135    /// Opens or creates a database, **automatically loading all existing collections**.
136    ///
137    /// This replaces the previous `open()` + `load_collections()` two-step pattern.
138    /// The new `open()` is a strict auto-load: all `config.json` directories under
139    /// `path` are loaded on startup.
140    ///
141    /// Uses the default [`VelesConfig`](crate::config::VelesConfig) — every
142    /// subsystem behaves identically to the pre-Wave-3 version of this
143    /// function, so existing callers keep their exact behaviour. Users
144    /// that need to customise subsystem limits or WAL batching should
145    /// call [`Database::open_with_config`] instead.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if the directory cannot be created or accessed.
150    pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
151        Self::open_impl(path, None, None)
152    }
153
154    /// Opens a database with an explicit [`VelesConfig`](crate::config::VelesConfig).
155    ///
156    /// Every subsystem that honours a config field (HNSW defaults, WAL
157    /// batching, runtime limits, search quality) reads from the passed
158    /// instance. A clone is stored inside the `Database` and retained
159    /// for the lifetime of the handle so sub-systems can consult it
160    /// without re-parsing a TOML file.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if the directory cannot be created, the lock
165    /// cannot be acquired, or any already-present collection exceeds
166    /// the limits declared in `config.limits` (see
167    /// [`Database::open`] for the default-limit behaviour).
168    pub fn open_with_config<P: AsRef<std::path::Path>>(
169        path: P,
170        config: crate::config::VelesConfig,
171    ) -> Result<Self> {
172        Self::open_impl(path, None, Some(config))
173    }
174
175    /// Opens a database with a [`DatabaseObserver`] (used by velesdb-premium).
176    ///
177    /// The observer receives lifecycle hooks for every collection operation,
178    /// enabling RBAC, audit logging, multi-tenant routing, etc.
179    ///
180    /// Equivalent to [`Database::open`] plus the observer injection —
181    /// applies the default [`VelesConfig`](crate::config::VelesConfig).
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if the directory cannot be created or accessed.
186    pub fn open_with_observer<P: AsRef<std::path::Path>>(
187        path: P,
188        observer: std::sync::Arc<dyn DatabaseObserver>,
189    ) -> Result<Self> {
190        Self::open_impl(path, Some(observer), None)
191    }
192
193    /// Opens a database with both an explicit [`crate::VelesConfig`] and a
194    /// [`crate::DatabaseObserver`]. Used by the premium shell that layers
195    /// RBAC/audit on top of a tenant-specific config file.
196    ///
197    /// # Errors
198    ///
199    /// Same as [`Database::open_with_config`].
200    pub fn open_with_observer_and_config<P: AsRef<std::path::Path>>(
201        path: P,
202        observer: std::sync::Arc<dyn DatabaseObserver>,
203        config: crate::config::VelesConfig,
204    ) -> Result<Self> {
205        Self::open_impl(path, Some(observer), Some(config))
206    }
207
208    fn open_impl<P: AsRef<std::path::Path>>(
209        path: P,
210        observer: Option<std::sync::Arc<dyn DatabaseObserver>>,
211        config: Option<crate::config::VelesConfig>,
212    ) -> Result<Self> {
213        // Validate at the consumption boundary: a `VelesConfig` built
214        // programmatically (not through a loader) never passes through
215        // `validate()`, so an out-of-range field would otherwise reach the
216        // engine unchecked. Loaders already validate, but re-validating here
217        // is cheap and closes the bypass for direct API callers.
218        let config = config.unwrap_or_default();
219        config
220            .validate()
221            .map_err(|e| Error::Config(e.to_string()))?;
222
223        let data_dir = path.as_ref().to_path_buf();
224        std::fs::create_dir_all(&data_dir)?;
225
226        // Acquire exclusive file lock to prevent multi-process corruption
227        let lock_path = data_dir.join("velesdb.lock");
228        let lock_file = std::fs::File::create(&lock_path)?;
229        fs2::FileExt::try_lock_exclusive(&lock_file)
230            .map_err(|_| Error::DatabaseLocked(data_dir.display().to_string()))?;
231
232        // Log SIMD features detected at startup
233        let features = simd_dispatch::simd_features_info();
234        tracing::info!(
235            avx512 = features.avx512f,
236            avx2 = features.avx2,
237            "SIMD features detected - direct dispatch enabled"
238        );
239
240        let db = Self {
241            data_dir,
242            _lock_file: lock_file,
243            config: std::sync::Arc::new(config),
244            vector_colls: parking_lot::RwLock::new(std::collections::HashMap::new()),
245            graph_colls: parking_lot::RwLock::new(std::collections::HashMap::new()),
246            metadata_colls: parking_lot::RwLock::new(std::collections::HashMap::new()),
247            collection_stats: parking_lot::RwLock::new(std::collections::HashMap::new()),
248            observer,
249            schema_version: std::sync::atomic::AtomicU64::new(0),
250            compiled_plan_cache: crate::cache::CompiledPlanCache::new(1_000, 10_000),
251            join_store_cache: parking_lot::RwLock::new(std::collections::HashMap::new()),
252        };
253
254        // Auto-load all existing collections from disk (replaces manual load_collections()).
255        db.load_collections()?;
256
257        Ok(db)
258    }
259
260    /// Returns a reference to the root [`VelesConfig`](crate::config::VelesConfig)
261    /// that was supplied at construction (or the default if the database
262    /// was opened via [`Database::open`]).
263    ///
264    /// Sub-systems (`vector_ops`, `query_engine`, `stats`, …) consult this
265    /// through `database.config()` when they need to honour a user-supplied
266    /// limit or toggle — the shared `Arc` makes the call free of locks
267    /// and cheap to propagate to background threads.
268    #[must_use]
269    pub fn config(&self) -> &crate::config::VelesConfig {
270        &self.config
271    }
272
273    /// Returns a cheap, cloneable handle to the root config.
274    ///
275    /// Use this when you need to move the config into a thread or
276    /// long-lived closure that outlives the current `&self` borrow.
277    #[must_use]
278    pub fn config_arc(&self) -> std::sync::Arc<crate::config::VelesConfig> {
279        std::sync::Arc::clone(&self.config)
280    }
281
282    /// Returns the path to the data directory.
283    #[must_use]
284    pub fn data_dir(&self) -> &std::path::Path {
285        &self.data_dir
286    }
287
288    /// Returns the current DDL schema version counter.
289    #[must_use]
290    pub fn schema_version(&self) -> u64 {
291        self.schema_version
292            .load(std::sync::atomic::Ordering::Relaxed)
293    }
294
295    /// Returns a reference to the compiled query plan cache.
296    #[must_use]
297    pub fn plan_cache(&self) -> &crate::cache::CompiledPlanCache {
298        &self.compiled_plan_cache
299    }
300
301    // =========================================================================
302    // Observer notification helpers (called by server handlers after operations)
303    // =========================================================================
304
305    /// Fires the `on_upsert` telemetry hook once for a completed upsert write.
306    ///
307    /// Called by the core use-case DML entry points (INSERT/UPSERT and UPDATE
308    /// in [`query_engine_dml`]) after the data-plane write completes, so a
309    /// single completed operation results in exactly one `on_upsert`
310    /// invocation (Requirement 2.1, 2.3, 2.5). This is the internal replacement
311    /// for the deprecated [`Database::notify_upsert`] shim — it is invoked by
312    /// core itself rather than by callers, and is a single pointer check when
313    /// no observer is present (Requirement 2.4).
314    pub(super) fn fire_on_upsert(&self, collection: &str, point_count: usize) {
315        if let Some(ref obs) = self.observer {
316            obs.on_upsert(collection, point_count);
317        }
318    }
319
320    /// Notifies the observer that points were upserted into a collection.
321    ///
322    /// **Caller contract**: this method is NOT called automatically by
323    /// [`Database`] internals. HTTP handlers and SDK bindings are responsible
324    /// for calling it after a successful upsert, passing the number of points
325    /// written. Forgetting to call it means the observer receives no upsert
326    /// events for that operation.
327    ///
328    /// No-op when no observer is registered.
329    #[deprecated(
330        since = "3.9.0",
331        note = "Telemetry is now invoked inside the core use-case layer. \
332                This shim is retained for backward compatibility and will be \
333                removed in a future major version. Callers should stop invoking \
334                it to avoid double-counting once they upgrade."
335    )]
336    pub fn notify_upsert(&self, collection: &str, point_count: usize) {
337        if let Some(ref obs) = self.observer {
338            obs.on_upsert(collection, point_count);
339        }
340    }
341
342    /// Notifies the observer that a query was executed, with its duration.
343    ///
344    /// **Caller contract**: this method is NOT called automatically by
345    /// [`Database::execute_query`]. Callers must measure the wall-clock
346    /// duration themselves (e.g. `std::time::Instant::now()` before the call)
347    /// and invoke this method afterwards with the elapsed microseconds.
348    ///
349    /// No-op when no observer is registered.
350    #[deprecated(
351        since = "3.9.0",
352        note = "See notify_upsert deprecation note. Telemetry is now invoked \
353                inside the core use-case layer; callers should stop invoking \
354                this shim to avoid double-counting once they upgrade."
355    )]
356    pub fn notify_query(&self, collection: &str, duration_us: u64) {
357        if let Some(ref obs) = self.observer {
358            obs.on_query(collection, duration_us);
359        }
360    }
361}