Skip to main content

cynos_database/
table.rs

1//! Table and TableBuilder for schema definition.
2//!
3//! This module provides the JavaScript API for creating and managing tables.
4
5use crate::expr::Column;
6use crate::JsDataType;
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9use cynos_core::schema::{Table, TableBuilder};
10use cynos_core::DataType;
11use wasm_bindgen::prelude::*;
12
13/// Column options for table creation.
14#[wasm_bindgen]
15#[derive(Clone, Debug, Default)]
16pub struct ColumnOptions {
17    pub primary_key: bool,
18    pub nullable: bool,
19    pub unique: bool,
20    pub auto_increment: bool,
21}
22
23#[wasm_bindgen]
24impl ColumnOptions {
25    #[wasm_bindgen(constructor)]
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    #[wasm_bindgen(js_name = primaryKey)]
31    pub fn set_primary_key(mut self, value: bool) -> Self {
32        self.primary_key = value;
33        self
34    }
35
36    #[wasm_bindgen(js_name = setNullable)]
37    pub fn set_nullable(mut self, value: bool) -> Self {
38        self.nullable = value;
39        self
40    }
41
42    #[wasm_bindgen(js_name = setUnique)]
43    pub fn set_unique(mut self, value: bool) -> Self {
44        self.unique = value;
45        self
46    }
47
48    #[wasm_bindgen(js_name = setAutoIncrement)]
49    pub fn set_auto_increment(mut self, value: bool) -> Self {
50        self.auto_increment = value;
51        self
52    }
53}
54
55/// JavaScript-friendly table builder.
56#[wasm_bindgen]
57pub struct JsTableBuilder {
58    name: String,
59    columns: Vec<ColumnDef>,
60    primary_key: Option<Vec<String>>,
61    indices: Vec<IndexDef>,
62    auto_increment: bool,
63}
64
65#[derive(Clone, Debug)]
66#[allow(dead_code)]
67struct ColumnDef {
68    name: String,
69    data_type: DataType,
70    nullable: bool,
71    unique: bool,
72}
73
74#[derive(Clone, Debug)]
75struct IndexDef {
76    name: String,
77    columns: Vec<String>,
78    unique: bool,
79}
80
81#[wasm_bindgen]
82impl JsTableBuilder {
83    /// Creates a new table builder.
84    #[wasm_bindgen(constructor)]
85    pub fn new(name: &str) -> Self {
86        Self {
87            name: name.to_string(),
88            columns: Vec::new(),
89            primary_key: None,
90            indices: Vec::new(),
91            auto_increment: false,
92        }
93    }
94
95    /// Builds the table schema and returns a JsTable.
96    pub fn build(&self) -> Result<JsTable, JsValue> {
97        let schema = self.build_internal()?;
98        Ok(JsTable::new(schema))
99    }
100
101    /// Adds a column to the table.
102    pub fn column(
103        mut self,
104        name: &str,
105        data_type: JsDataType,
106        options: Option<ColumnOptions>,
107    ) -> Self {
108        let opts = options.unwrap_or_default();
109
110        self.columns.push(ColumnDef {
111            name: name.to_string(),
112            data_type: data_type.into(),
113            nullable: opts.nullable,
114            unique: opts.unique || opts.primary_key,
115        });
116
117        if opts.primary_key {
118            self.primary_key = Some(alloc::vec![name.to_string()]);
119            self.auto_increment = opts.auto_increment;
120        }
121
122        self
123    }
124
125    /// Sets the primary key columns.
126    #[wasm_bindgen(js_name = primaryKey)]
127    pub fn primary_key(mut self, columns: &JsValue) -> Self {
128        if let Some(arr) = columns.dyn_ref::<js_sys::Array>() {
129            let cols: Vec<String> = arr
130                .iter()
131                .filter_map(|v| v.as_string())
132                .collect();
133            self.primary_key = Some(cols);
134        } else if let Some(s) = columns.as_string() {
135            self.primary_key = Some(alloc::vec![s]);
136        }
137        self
138    }
139
140    /// Adds an index to the table.
141    pub fn index(mut self, name: &str, columns: &JsValue) -> Self {
142        let cols = if let Some(arr) = columns.dyn_ref::<js_sys::Array>() {
143            arr.iter().filter_map(|v| v.as_string()).collect()
144        } else if let Some(s) = columns.as_string() {
145            alloc::vec![s]
146        } else {
147            return self;
148        };
149
150        self.indices.push(IndexDef {
151            name: name.to_string(),
152            columns: cols,
153            unique: false,
154        });
155        self
156    }
157
158    /// Adds a unique index to the table.
159    #[wasm_bindgen(js_name = uniqueIndex)]
160    pub fn unique_index(mut self, name: &str, columns: &JsValue) -> Self {
161        let cols = if let Some(arr) = columns.dyn_ref::<js_sys::Array>() {
162            arr.iter().filter_map(|v| v.as_string()).collect()
163        } else if let Some(s) = columns.as_string() {
164            alloc::vec![s]
165        } else {
166            return self;
167        };
168
169        self.indices.push(IndexDef {
170            name: name.to_string(),
171            columns: cols,
172            unique: true,
173        });
174        self
175    }
176
177    /// Adds a JSONB index for specific paths.
178    #[wasm_bindgen(js_name = jsonbIndex)]
179    pub fn jsonb_index(mut self, column: &str, _paths: &JsValue) -> Self {
180        // JSONB indices are handled specially - for now just create a regular index
181        // The actual JSONB indexing is done at the storage layer
182        let name = alloc::format!("idx_jsonb_{}", column);
183        self.indices.push(IndexDef {
184            name,
185            columns: alloc::vec![column.to_string()],
186            unique: false,
187        });
188        self
189    }
190
191    /// Builds the table schema (internal use).
192    pub(crate) fn build_internal(&self) -> Result<Table, JsValue> {
193        let mut builder = TableBuilder::new(&self.name)
194            .map_err(|e| JsValue::from_str(&alloc::format!("{:?}", e)))?;
195
196        // Add columns
197        for col in &self.columns {
198            builder = builder
199                .add_column(&col.name, col.data_type)
200                .map_err(|e| JsValue::from_str(&alloc::format!("{:?}", e)))?;
201
202            if col.nullable {
203                builder = builder.add_nullable(&[col.name.as_str()]);
204            }
205        }
206
207        // Add primary key
208        if let Some(pk_cols) = &self.primary_key {
209            let pk_refs: Vec<&str> = pk_cols.iter().map(|s| s.as_str()).collect();
210            builder = builder
211                .add_primary_key(&pk_refs, self.auto_increment)
212                .map_err(|e| JsValue::from_str(&alloc::format!("{:?}", e)))?;
213        }
214
215        // Add indices
216        for idx in &self.indices {
217            let col_refs: Vec<&str> = idx.columns.iter().map(|s| s.as_str()).collect();
218            builder = builder
219                .add_index(&idx.name, &col_refs, idx.unique)
220                .map_err(|e| JsValue::from_str(&alloc::format!("{:?}", e)))?;
221        }
222
223        builder
224            .build()
225            .map_err(|e| JsValue::from_str(&alloc::format!("{:?}", e)))
226    }
227
228    /// Returns the table name.
229    #[wasm_bindgen(getter)]
230    pub fn name(&self) -> String {
231        self.name.clone()
232    }
233}
234
235/// JavaScript-friendly table reference.
236#[wasm_bindgen]
237pub struct JsTable {
238    schema: Table,
239}
240
241impl JsTable {
242    pub fn new(schema: Table) -> Self {
243        Self { schema }
244    }
245
246    pub fn schema(&self) -> &Table {
247        &self.schema
248    }
249}
250
251#[wasm_bindgen]
252impl JsTable {
253    /// Returns the table name.
254    #[wasm_bindgen(getter)]
255    pub fn name(&self) -> String {
256        self.schema.name().to_string()
257    }
258
259    /// Returns a column reference.
260    pub fn col(&self, name: &str) -> Option<Column> {
261        self.schema.get_column(name).map(|c| {
262            Column::new(self.schema.name(), c.name()).with_index(c.index())
263        })
264    }
265
266    /// Returns the column names.
267    #[wasm_bindgen(js_name = columnNames)]
268    pub fn column_names(&self) -> js_sys::Array {
269        let arr = js_sys::Array::new();
270        for col in self.schema.columns() {
271            arr.push(&JsValue::from_str(col.name()));
272        }
273        arr
274    }
275
276    /// Returns the number of columns.
277    #[wasm_bindgen(js_name = columnCount)]
278    pub fn column_count(&self) -> usize {
279        self.schema.columns().len()
280    }
281
282    /// Returns the column data type.
283    #[wasm_bindgen(js_name = getColumnType)]
284    pub fn get_column_type(&self, name: &str) -> Option<JsDataType> {
285        self.schema.get_column(name).map(|c| c.data_type().into())
286    }
287
288    /// Returns whether a column is nullable.
289    #[wasm_bindgen(js_name = isColumnNullable)]
290    pub fn is_column_nullable(&self, name: &str) -> bool {
291        self.schema
292            .get_column(name)
293            .map(|c| c.is_nullable())
294            .unwrap_or(false)
295    }
296
297    /// Returns the primary key column names.
298    #[wasm_bindgen(js_name = primaryKeyColumns)]
299    pub fn primary_key_columns(&self) -> js_sys::Array {
300        let arr = js_sys::Array::new();
301        if let Some(pk) = self.schema.primary_key() {
302            for col in pk.columns() {
303                arr.push(&JsValue::from_str(&col.name));
304            }
305        }
306        arr
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use wasm_bindgen_test::*;
314
315    wasm_bindgen_test_configure!(run_in_browser);
316
317    #[wasm_bindgen_test]
318    fn test_table_builder_basic() {
319        let builder = JsTableBuilder::new("users")
320            .column("id", JsDataType::Int64, Some(ColumnOptions::new().set_primary_key(true)))
321            .column("name", JsDataType::String, None)
322            .column("age", JsDataType::Int32, None);
323
324        let table = builder.build_internal().unwrap();
325        assert_eq!(table.name(), "users");
326        assert_eq!(table.columns().len(), 3);
327    }
328
329    #[wasm_bindgen_test]
330    fn test_table_builder_with_index() {
331        let builder = JsTableBuilder::new("users")
332            .column("id", JsDataType::Int64, Some(ColumnOptions::new().set_primary_key(true)))
333            .column("email", JsDataType::String, None)
334            .unique_index("idx_email", &JsValue::from_str("email"));
335
336        let table = builder.build_internal().unwrap();
337        assert!(table.indices().iter().any(|i| i.name() == "idx_email"));
338    }
339
340    #[wasm_bindgen_test]
341    fn test_table_builder_nullable() {
342        let builder = JsTableBuilder::new("users")
343            .column("id", JsDataType::Int64, Some(ColumnOptions::new().set_primary_key(true)))
344            .column("bio", JsDataType::String, Some(ColumnOptions::new().set_nullable(true)));
345
346        let table = builder.build_internal().unwrap();
347        let bio_col = table.get_column("bio").unwrap();
348        assert!(bio_col.is_nullable());
349    }
350
351    #[wasm_bindgen_test]
352    fn test_js_table_col() {
353        let builder = JsTableBuilder::new("users")
354            .column("id", JsDataType::Int64, Some(ColumnOptions::new().set_primary_key(true)))
355            .column("name", JsDataType::String, None);
356
357        let schema = builder.build_internal().unwrap();
358        let table = JsTable::new(schema);
359
360        let col = table.col("name").unwrap();
361        assert_eq!(col.name(), "name");
362    }
363
364    #[wasm_bindgen_test]
365    fn test_js_table_column_names() {
366        let builder = JsTableBuilder::new("users")
367            .column("id", JsDataType::Int64, Some(ColumnOptions::new().set_primary_key(true)))
368            .column("name", JsDataType::String, None)
369            .column("age", JsDataType::Int32, None);
370
371        let schema = builder.build_internal().unwrap();
372        let table = JsTable::new(schema);
373
374        let names = table.column_names();
375        assert_eq!(names.length(), 3);
376    }
377}