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}