toondb/
schema.rs

1// Copyright 2025 Sushanth (https://github.com/sushanthpy)
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Schema Management (DDL)
16//!
17//! Provides fluent schema definition and DDL operations.
18
19use crate::connection::{ToonConnection, to_field_type};
20use crate::error::{ClientError, Result};
21
22use toondb_core::toon::{ToonSchema, ToonType};
23
24/// Schema builder with fluent API
25pub struct SchemaBuilder {
26    name: String,
27    fields: Vec<FieldBuilder>,
28    primary_key: Option<String>,
29    indexes: Vec<IndexDef>,
30}
31
32/// Field builder
33#[derive(Debug, Clone)]
34pub struct FieldBuilder {
35    pub name: String,
36    pub field_type: ToonType,
37    pub nullable: bool,
38    pub unique: bool,
39    pub default: Option<String>,
40}
41
42/// Index definition
43#[derive(Debug, Clone)]
44pub struct IndexDef {
45    pub name: String,
46    pub fields: Vec<String>,
47    pub unique: bool,
48}
49
50impl SchemaBuilder {
51    /// Start building a new table schema
52    pub fn table(name: &str) -> Self {
53        Self {
54            name: name.to_string(),
55            fields: Vec::new(),
56            primary_key: None,
57            indexes: Vec::new(),
58        }
59    }
60
61    /// Add a field
62    pub fn field(mut self, name: &str, field_type: ToonType) -> FieldConfig {
63        self.fields.push(FieldBuilder {
64            name: name.to_string(),
65            field_type,
66            nullable: true,
67            unique: false,
68            default: None,
69        });
70        FieldConfig { builder: self }
71    }
72
73    /// Set primary key
74    pub fn primary_key(mut self, field: &str) -> Self {
75        self.primary_key = Some(field.to_string());
76        self
77    }
78
79    /// Add an index
80    pub fn index(mut self, name: &str, fields: &[&str], unique: bool) -> Self {
81        self.indexes.push(IndexDef {
82            name: name.to_string(),
83            fields: fields.iter().map(|s| s.to_string()).collect(),
84            unique,
85        });
86        self
87    }
88
89    /// Build the ToonSchema
90    pub fn build(self) -> ToonSchema {
91        let mut schema = ToonSchema::new(&self.name);
92        for field in self.fields {
93            schema = schema.field(&field.name, field.field_type);
94        }
95        if let Some(pk) = self.primary_key {
96            schema = schema.primary_key(&pk);
97        }
98        schema
99    }
100
101    /// Get field definitions
102    pub fn get_fields(&self) -> &[FieldBuilder] {
103        &self.fields
104    }
105}
106
107/// Field configuration (for chaining)
108pub struct FieldConfig {
109    pub(crate) builder: SchemaBuilder,
110}
111
112impl FieldConfig {
113    /// Mark field as NOT NULL
114    pub fn not_null(mut self) -> Self {
115        if let Some(field) = self.builder.fields.last_mut() {
116            field.nullable = false;
117        }
118        self
119    }
120
121    /// Mark field as UNIQUE
122    pub fn unique(mut self) -> Self {
123        if let Some(field) = self.builder.fields.last_mut() {
124            field.unique = true;
125        }
126        self
127    }
128
129    /// Set default value
130    pub fn default_value(mut self, value: &str) -> Self {
131        if let Some(field) = self.builder.fields.last_mut() {
132            field.default = Some(value.to_string());
133        }
134        self
135    }
136
137    /// Continue adding fields
138    pub fn field(self, name: &str, field_type: ToonType) -> FieldConfig {
139        self.builder.field(name, field_type)
140    }
141
142    /// Finish with primary key
143    pub fn primary_key(self, field: &str) -> SchemaBuilder {
144        self.builder.primary_key(field)
145    }
146
147    /// Add index
148    pub fn index(self, name: &str, fields: &[&str], unique: bool) -> SchemaBuilder {
149        self.builder.index(name, fields, unique)
150    }
151
152    /// Build schema
153    pub fn build(self) -> ToonSchema {
154        self.builder.build()
155    }
156}
157
158/// Table description
159#[derive(Debug, Clone)]
160pub struct TableDescription {
161    pub name: String,
162    pub columns: Vec<ColumnDescription>,
163    pub row_count: u64,
164    pub indexes: Vec<String>,
165}
166
167/// Column description
168#[derive(Debug, Clone)]
169pub struct ColumnDescription {
170    pub name: String,
171    pub field_type: ToonType,
172    pub nullable: bool,
173}
174
175/// Create table result
176#[derive(Debug, Clone)]
177pub struct CreateTableResult {
178    pub table_name: String,
179    pub column_count: usize,
180}
181
182/// Drop table result
183#[derive(Debug, Clone)]
184pub struct DropTableResult {
185    pub table_name: String,
186    pub rows_deleted: u64,
187}
188
189/// Create index result
190#[derive(Debug, Clone)]
191pub struct CreateIndexResult {
192    pub index_name: String,
193    pub table_name: String,
194    pub rows_indexed: usize,
195}
196
197/// DDL operations on connection
198impl ToonConnection {
199    /// Create a new table from schema
200    pub fn create_table(&self, schema: ToonSchema) -> Result<CreateTableResult> {
201        let name = schema.name.clone();
202        let column_count = schema.fields.len();
203
204        // Register in TCH
205        let fields: Vec<_> = schema
206            .fields
207            .iter()
208            .map(|f| (f.name.clone(), to_field_type(&f.field_type)))
209            .collect();
210        self.tch.write().register_table(&name, &fields);
211
212        // Update catalog
213        {
214            let mut catalog = self.catalog.write();
215            let root_id = 0; // Placeholder
216            catalog
217                .create_table(schema, root_id)
218                .map_err(ClientError::Schema)?;
219        }
220
221        Ok(CreateTableResult {
222            table_name: name,
223            column_count,
224        })
225    }
226
227    /// Drop a table
228    pub fn drop_table(&self, name: &str) -> Result<DropTableResult> {
229        let entry = {
230            let mut catalog = self.catalog.write();
231            catalog.drop_table(name).map_err(ClientError::Schema)?
232        };
233
234        Ok(DropTableResult {
235            table_name: name.to_string(),
236            rows_deleted: entry.row_count,
237        })
238    }
239
240    /// Create an index
241    pub fn create_index(
242        &self,
243        name: &str,
244        table: &str,
245        fields: &[&str],
246        unique: bool,
247    ) -> Result<CreateIndexResult> {
248        {
249            let mut catalog = self.catalog.write();
250            let root_id = 0; // Placeholder
251            catalog
252                .create_index(
253                    name,
254                    table,
255                    fields.iter().map(|s| s.to_string()).collect(),
256                    unique,
257                    root_id,
258                )
259                .map_err(ClientError::Schema)?;
260        }
261
262        Ok(CreateIndexResult {
263            index_name: name.to_string(),
264            table_name: table.to_string(),
265            rows_indexed: 0, // Placeholder
266        })
267    }
268
269    /// Drop an index
270    pub fn drop_index(&self, name: &str) -> Result<()> {
271        let mut catalog = self.catalog.write();
272        catalog.drop_index(name).map_err(ClientError::Schema)?;
273        Ok(())
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_schema_builder() {
283        let schema = SchemaBuilder::table("users")
284            .field("id", ToonType::Int)
285            .not_null()
286            .field("name", ToonType::Text)
287            .not_null()
288            .field("email", ToonType::Text)
289            .unique()
290            .primary_key("id")
291            .build();
292
293        assert_eq!(schema.name, "users");
294        assert_eq!(schema.fields.len(), 3);
295    }
296
297    #[test]
298    fn test_schema_with_index() {
299        let schema = SchemaBuilder::table("orders")
300            .field("id", ToonType::Int)
301            .field("user_id", ToonType::Int)
302            .field("amount", ToonType::Float)
303            .primary_key("id")
304            .index("idx_user", &["user_id"], false)
305            .build();
306
307        assert_eq!(schema.name, "orders");
308    }
309
310    #[test]
311    fn test_create_table() {
312        let conn = ToonConnection::open("./test").unwrap();
313
314        let schema = SchemaBuilder::table("test_table")
315            .field("id", ToonType::Int)
316            .field("name", ToonType::Text)
317            .build();
318
319        let result = conn.create_table(schema).unwrap();
320        assert_eq!(result.table_name, "test_table");
321        assert_eq!(result.column_count, 2);
322    }
323
324    #[test]
325    fn test_drop_table() {
326        let conn = ToonConnection::open("./test").unwrap();
327
328        let schema = SchemaBuilder::table("to_drop")
329            .field("id", ToonType::Int)
330            .build();
331
332        conn.create_table(schema).unwrap();
333        let result = conn.drop_table("to_drop").unwrap();
334        assert_eq!(result.table_name, "to_drop");
335    }
336}