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