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}