Skip to main content

cynos_database/
database.rs

1//! Database - Main entry point for Cynos database operations.
2//!
3//! This module provides the `Database` struct which is the primary interface
4//! for creating tables, executing queries, and managing data.
5
6use crate::binary_protocol::SchemaLayoutCache;
7use crate::query_builder::{DeleteBuilder, InsertBuilder, SelectBuilder, UpdateBuilder};
8use crate::reactive_bridge::QueryRegistry;
9use crate::table::{JsTable, JsTableBuilder};
10use crate::transaction::JsTransaction;
11use alloc::rc::Rc;
12use alloc::string::{String, ToString};
13#[cfg(feature = "benchmark")]
14use alloc::vec::Vec;
15#[cfg(feature = "benchmark")]
16use cynos_core::Row;
17#[allow(unused_imports)]
18use cynos_incremental::Delta;
19use cynos_query::plan_cache::PlanCache;
20use cynos_reactive::TableId;
21use cynos_storage::TableCache;
22use core::cell::RefCell;
23use wasm_bindgen::prelude::*;
24
25/// The main database interface.
26///
27/// Provides methods for:
28/// - Creating and dropping tables
29/// - CRUD operations (insert, select, update, delete)
30/// - Transaction management
31/// - Observable queries
32#[wasm_bindgen]
33pub struct Database {
34    name: String,
35    cache: Rc<RefCell<TableCache>>,
36    query_registry: Rc<RefCell<QueryRegistry>>,
37    table_id_map: Rc<RefCell<hashbrown::HashMap<String, TableId>>>,
38    next_table_id: Rc<RefCell<TableId>>,
39    schema_layout_cache: Rc<RefCell<SchemaLayoutCache>>,
40    plan_cache: Rc<RefCell<PlanCache>>,
41}
42
43#[wasm_bindgen]
44impl Database {
45    /// Creates a new database instance.
46    #[wasm_bindgen(constructor)]
47    pub fn new(name: &str) -> Self {
48        let query_registry = Rc::new(RefCell::new(QueryRegistry::new()));
49        // Set self reference for microtask scheduling
50        query_registry.borrow_mut().set_self_ref(query_registry.clone());
51
52        Self {
53            name: name.to_string(),
54            cache: Rc::new(RefCell::new(TableCache::new())),
55            query_registry,
56            table_id_map: Rc::new(RefCell::new(hashbrown::HashMap::new())),
57            next_table_id: Rc::new(RefCell::new(1)),
58            schema_layout_cache: Rc::new(RefCell::new(SchemaLayoutCache::new())),
59            plan_cache: Rc::new(RefCell::new(PlanCache::default_size())),
60        }
61    }
62
63    /// Async factory method for creating a database (for WASM compatibility).
64    #[wasm_bindgen(js_name = create)]
65    pub async fn create(name: &str) -> Result<Database, JsValue> {
66        Ok(Self::new(name))
67    }
68
69    /// Returns the database name.
70    #[wasm_bindgen(getter)]
71    pub fn name(&self) -> String {
72        self.name.clone()
73    }
74
75    /// Creates a new table builder.
76    #[wasm_bindgen(js_name = createTable)]
77    pub fn create_table(&self, name: &str) -> JsTableBuilder {
78        JsTableBuilder::new(name)
79    }
80
81    /// Registers a table schema with the database.
82    #[wasm_bindgen(js_name = registerTable)]
83    pub fn register_table(&self, builder: &JsTableBuilder) -> Result<(), JsValue> {
84        let schema = builder.build_internal()?;
85        let table_name = schema.name().to_string();
86
87        self.cache
88            .borrow_mut()
89            .create_table(schema)
90            .map_err(|e| JsValue::from_str(&alloc::format!("{:?}", e)))?;
91
92        // Assign table ID
93        let table_id = *self.next_table_id.borrow();
94        *self.next_table_id.borrow_mut() += 1;
95        self.table_id_map.borrow_mut().insert(table_name, table_id);
96
97        Ok(())
98    }
99
100    /// Gets a table reference by name.
101    pub fn table(&self, name: &str) -> Option<JsTable> {
102        self.cache
103            .borrow()
104            .get_table(name)
105            .map(|store| JsTable::new(store.schema().clone()))
106    }
107
108    /// Drops a table from the database.
109    #[wasm_bindgen(js_name = dropTable)]
110    pub fn drop_table(&self, name: &str) -> Result<(), JsValue> {
111        self.cache
112            .borrow_mut()
113            .drop_table(name)
114            .map_err(|e| JsValue::from_str(&alloc::format!("{:?}", e)))?;
115
116        self.table_id_map.borrow_mut().remove(name);
117        Ok(())
118    }
119
120    /// Returns all table names.
121    #[wasm_bindgen(js_name = tableNames)]
122    pub fn table_names(&self) -> js_sys::Array {
123        let arr = js_sys::Array::new();
124        for name in self.cache.borrow().table_names() {
125            arr.push(&JsValue::from_str(name));
126        }
127        arr
128    }
129
130    /// Returns the number of tables.
131    #[wasm_bindgen(js_name = tableCount)]
132    pub fn table_count(&self) -> usize {
133        self.cache.borrow().table_count()
134    }
135
136    /// Starts a SELECT query.
137    /// Accepts either:
138    /// - A single string: select('*') or select('name')
139    /// - Multiple strings: select('name', 'score') - passed as variadic args
140    #[wasm_bindgen(variadic)]
141    pub fn select(&self, columns: &JsValue) -> SelectBuilder {
142        SelectBuilder::new(
143            self.cache.clone(),
144            self.query_registry.clone(),
145            self.table_id_map.clone(),
146            self.schema_layout_cache.clone(),
147            self.plan_cache.clone(),
148            columns.clone(),
149        )
150    }
151
152    /// Starts an INSERT operation.
153    pub fn insert(&self, table: &str) -> InsertBuilder {
154        InsertBuilder::new(
155            self.cache.clone(),
156            self.query_registry.clone(),
157            self.table_id_map.clone(),
158            table,
159        )
160    }
161
162    /// Starts an UPDATE operation.
163    pub fn update(&self, table: &str) -> UpdateBuilder {
164        UpdateBuilder::new(
165            self.cache.clone(),
166            self.query_registry.clone(),
167            self.table_id_map.clone(),
168            table,
169        )
170    }
171
172    /// Starts a DELETE operation.
173    pub fn delete(&self, table: &str) -> DeleteBuilder {
174        DeleteBuilder::new(
175            self.cache.clone(),
176            self.query_registry.clone(),
177            self.table_id_map.clone(),
178            table,
179        )
180    }
181
182    /// Begins a new transaction.
183    pub fn transaction(&self) -> JsTransaction {
184        JsTransaction::new(
185            self.cache.clone(),
186            self.query_registry.clone(),
187            self.table_id_map.clone(),
188        )
189    }
190
191    /// Clears all data from all tables.
192    pub fn clear(&self) {
193        self.cache.borrow_mut().clear();
194    }
195
196    /// Clears data from a specific table.
197    #[wasm_bindgen(js_name = clearTable)]
198    pub fn clear_table(&self, name: &str) -> Result<(), JsValue> {
199        self.cache
200            .borrow_mut()
201            .clear_table(name)
202            .map_err(|e| JsValue::from_str(&alloc::format!("{:?}", e)))
203    }
204
205    /// Returns the total row count across all tables.
206    #[wasm_bindgen(js_name = totalRowCount)]
207    pub fn total_row_count(&self) -> usize {
208        self.cache.borrow().total_row_count()
209    }
210
211    /// Checks if a table exists.
212    #[wasm_bindgen(js_name = hasTable)]
213    pub fn has_table(&self, name: &str) -> bool {
214        self.cache.borrow().has_table(name)
215    }
216
217    /// Benchmarks pure Rust insert performance without JS serialization overhead.
218    ///
219    /// This method generates and inserts `count` rows directly in Rust,
220    /// measuring only the storage layer performance.
221    ///
222    /// Returns an object with:
223    /// - `duration_ms`: Total time in milliseconds
224    /// - `rows_per_sec`: Throughput in rows per second
225    #[cfg(feature = "benchmark")]
226    #[wasm_bindgen(js_name = benchmarkInsert)]
227    pub fn benchmark_insert(&self, table: &str, count: u32) -> Result<JsValue, JsValue> {
228        use cynos_core::Value;
229
230        let mut cache = self.cache.borrow_mut();
231        let store = cache
232            .get_table_mut(table)
233            .ok_or_else(|| JsValue::from_str(&alloc::format!("Table not found: {}", table)))?;
234
235        let schema = store.schema().clone();
236        let columns = schema.columns();
237
238        // Generate rows in Rust (no JS serialization)
239        let start = js_sys::Date::now();
240
241        for i in 0..count {
242            let row_id = cynos_core::next_row_id();
243            let mut values = Vec::with_capacity(columns.len());
244
245            for (col_idx, col) in columns.iter().enumerate() {
246                let value = match col.data_type() {
247                    cynos_core::DataType::Int64 => {
248                        if col_idx == 0 {
249                            // Primary key - use sequential ID
250                            Value::Int64(i as i64 + 1)
251                        } else {
252                            Value::Int64((i % 1000) as i64)
253                        }
254                    }
255                    cynos_core::DataType::Int32 => Value::Int32((i % 100) as i32),
256                    cynos_core::DataType::String => Value::String(alloc::format!("value_{}", i)),
257                    cynos_core::DataType::Boolean => Value::Boolean(i % 2 == 0),
258                    cynos_core::DataType::Float64 => Value::Float64(i as f64 * 0.1),
259                    cynos_core::DataType::DateTime => Value::DateTime(1700000000000 + i as i64 * 1000),
260                    _ => Value::Null,
261                };
262                values.push(value);
263            }
264
265            let row = Row::new(row_id, values);
266            store.insert(row).map_err(|e| JsValue::from_str(&alloc::format!("{:?}", e)))?;
267        }
268
269        let end = js_sys::Date::now();
270        let duration_ms = end - start;
271        let rows_per_sec = if duration_ms > 0.0 {
272            (count as f64 / duration_ms) * 1000.0
273        } else {
274            f64::INFINITY
275        };
276
277        // Return result object
278        let result = js_sys::Object::new();
279        js_sys::Reflect::set(&result, &JsValue::from_str("duration_ms"), &JsValue::from_f64(duration_ms))?;
280        js_sys::Reflect::set(&result, &JsValue::from_str("rows_per_sec"), &JsValue::from_f64(rows_per_sec))?;
281        js_sys::Reflect::set(&result, &JsValue::from_str("count"), &JsValue::from_f64(count as f64))?;
282
283        Ok(result.into())
284    }
285
286    /// Benchmarks pure Rust range query performance without JS serialization overhead.
287    ///
288    /// This method executes a range query (column > threshold) directly in Rust,
289    /// measuring only the query execution time without serialization to JS.
290    ///
291    /// Parameters:
292    /// - `table`: Table name to query
293    /// - `column`: Column name for the range condition
294    /// - `threshold`: The threshold value (column > threshold)
295    ///
296    /// Returns an object with:
297    /// - `query_ms`: Time for query execution only (no serialization)
298    /// - `serialize_ms`: Time for serialization to JS
299    /// - `total_ms`: Total time including serialization
300    /// - `row_count`: Number of rows returned
301    /// - `serialization_overhead_pct`: Percentage of time spent on serialization
302    #[cfg(feature = "benchmark")]
303    #[wasm_bindgen(js_name = benchmarkRangeQuery)]
304    pub fn benchmark_range_query(
305        &self,
306        table: &str,
307        column: &str,
308        threshold: f64,
309    ) -> Result<JsValue, JsValue> {
310        use crate::query_engine::execute_plan;
311        use cynos_query::planner::LogicalPlan;
312        use cynos_query::ast::{Expr as AstExpr, BinaryOp};
313
314        let cache = self.cache.borrow();
315        let store = cache
316            .get_table(table)
317            .ok_or_else(|| JsValue::from_str(&alloc::format!("Table not found: {}", table)))?;
318
319        let schema = store.schema().clone();
320        let col = schema
321            .get_column(column)
322            .ok_or_else(|| JsValue::from_str(&alloc::format!("Column not found: {}", column)))?;
323        let col_idx = col.index();
324
325        // Build logical plan: SELECT * FROM table WHERE column > threshold
326        let scan = LogicalPlan::Scan {
327            table: table.to_string(),
328        };
329
330        let predicate = AstExpr::BinaryOp {
331            left: Box::new(AstExpr::column(table, column, col_idx)),
332            op: BinaryOp::Gt,
333            right: Box::new(AstExpr::Literal(cynos_core::Value::Int64(threshold as i64))),
334        };
335
336        let plan = LogicalPlan::Filter {
337            input: Box::new(scan),
338            predicate,
339        };
340
341        // Measure query execution time (no serialization)
342        let query_start = js_sys::Date::now();
343        let rows = execute_plan(&cache, table, plan)
344            .map_err(|e| JsValue::from_str(&alloc::format!("Query error: {:?}", e)))?;
345        let query_end = js_sys::Date::now();
346        let query_ms = query_end - query_start;
347
348        let row_count = rows.len();
349
350        // Measure serialization time
351        let serialize_start = js_sys::Date::now();
352        let _js_result = crate::convert::rows_to_js_array(&rows, &schema);
353        let serialize_end = js_sys::Date::now();
354        let serialize_ms = serialize_end - serialize_start;
355
356        let total_ms = query_ms + serialize_ms;
357        let serialization_overhead_pct = if total_ms > 0.0 {
358            (serialize_ms / total_ms) * 100.0
359        } else {
360            0.0
361        };
362
363        // Return result object
364        let result = js_sys::Object::new();
365        js_sys::Reflect::set(&result, &JsValue::from_str("query_ms"), &JsValue::from_f64(query_ms))?;
366        js_sys::Reflect::set(&result, &JsValue::from_str("serialize_ms"), &JsValue::from_f64(serialize_ms))?;
367        js_sys::Reflect::set(&result, &JsValue::from_str("total_ms"), &JsValue::from_f64(total_ms))?;
368        js_sys::Reflect::set(&result, &JsValue::from_str("row_count"), &JsValue::from_f64(row_count as f64))?;
369        js_sys::Reflect::set(&result, &JsValue::from_str("serialization_overhead_pct"), &JsValue::from_f64(serialization_overhead_pct))?;
370
371        Ok(result.into())
372    }
373}
374
375#[allow(dead_code)]
376impl Database {
377    /// Gets the internal cache (for internal use).
378    pub(crate) fn cache(&self) -> Rc<RefCell<TableCache>> {
379        self.cache.clone()
380    }
381
382    /// Gets the query registry (for internal use).
383    pub(crate) fn query_registry(&self) -> Rc<RefCell<QueryRegistry>> {
384        self.query_registry.clone()
385    }
386
387    /// Gets the table ID for a table name.
388    pub(crate) fn get_table_id(&self, name: &str) -> Option<TableId> {
389        self.table_id_map.borrow().get(name).copied()
390    }
391
392    /// Notifies the query registry of table changes.
393    pub(crate) fn notify_table_change(&self, table_id: TableId, changed_ids: &hashbrown::HashSet<u64>) {
394        self.query_registry
395            .borrow_mut()
396            .on_table_change(table_id, changed_ids);
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::table::ColumnOptions;
404    use crate::JsDataType;
405    use wasm_bindgen_test::*;
406
407    wasm_bindgen_test_configure!(run_in_browser);
408
409    #[wasm_bindgen_test]
410    fn test_database_new() {
411        let db = Database::new("test");
412        assert_eq!(db.name(), "test");
413        assert_eq!(db.table_count(), 0);
414    }
415
416    #[wasm_bindgen_test]
417    fn test_database_create_table() {
418        let db = Database::new("test");
419
420        let builder = db
421            .create_table("users")
422            .column(
423                "id",
424                JsDataType::Int64,
425                Some(ColumnOptions::new().set_primary_key(true)),
426            )
427            .column("name", JsDataType::String, None);
428
429        db.register_table(&builder).unwrap();
430
431        assert!(db.has_table("users"));
432        assert_eq!(db.table_count(), 1);
433    }
434
435    #[wasm_bindgen_test]
436    fn test_database_drop_table() {
437        let db = Database::new("test");
438
439        let builder = db
440            .create_table("users")
441            .column(
442                "id",
443                JsDataType::Int64,
444                Some(ColumnOptions::new().set_primary_key(true)),
445            );
446
447        db.register_table(&builder).unwrap();
448        assert!(db.has_table("users"));
449
450        db.drop_table("users").unwrap();
451        assert!(!db.has_table("users"));
452    }
453
454    #[wasm_bindgen_test]
455    fn test_database_table_names() {
456        let db = Database::new("test");
457
458        let builder1 = db
459            .create_table("users")
460            .column(
461                "id",
462                JsDataType::Int64,
463                Some(ColumnOptions::new().set_primary_key(true)),
464            );
465        db.register_table(&builder1).unwrap();
466
467        let builder2 = db
468            .create_table("orders")
469            .column(
470                "id",
471                JsDataType::Int64,
472                Some(ColumnOptions::new().set_primary_key(true)),
473            );
474        db.register_table(&builder2).unwrap();
475
476        let names = db.table_names();
477        assert_eq!(names.length(), 2);
478    }
479
480    #[wasm_bindgen_test]
481    fn test_database_get_table() {
482        let db = Database::new("test");
483
484        let builder = db
485            .create_table("users")
486            .column(
487                "id",
488                JsDataType::Int64,
489                Some(ColumnOptions::new().set_primary_key(true)),
490            )
491            .column("name", JsDataType::String, None);
492
493        db.register_table(&builder).unwrap();
494
495        let table = db.table("users").unwrap();
496        assert_eq!(table.name(), "users");
497        assert_eq!(table.column_count(), 2);
498    }
499
500    #[wasm_bindgen_test]
501    fn test_database_clear() {
502        let db = Database::new("test");
503
504        let builder = db
505            .create_table("users")
506            .column(
507                "id",
508                JsDataType::Int64,
509                Some(ColumnOptions::new().set_primary_key(true)),
510            );
511
512        db.register_table(&builder).unwrap();
513
514        db.clear();
515        assert_eq!(db.total_row_count(), 0);
516        // Tables still exist after clear
517        assert!(db.has_table("users"));
518    }
519}