oxide_sql_core/migrations/
table_builder.rs

1//! Type-safe table builders using the typestate pattern.
2//!
3//! This module provides builders for table operations that enforce correct
4//! usage at compile time. For example, you cannot call `build()` on a
5//! `CreateTableBuilder` without first setting a name and at least one column.
6
7use std::marker::PhantomData;
8
9use super::column_builder::ColumnDefinition;
10use super::operation::{CreateTableOp, DropTableOp, TableConstraint};
11
12// =============================================================================
13// Typestate Markers
14// =============================================================================
15
16/// Marker: table has no name set.
17#[derive(Debug, Clone, Copy)]
18pub struct NoName;
19
20/// Marker: table has a name set.
21#[derive(Debug, Clone, Copy)]
22pub struct HasName;
23
24/// Marker: table has no columns.
25#[derive(Debug, Clone, Copy)]
26pub struct NoColumns;
27
28/// Marker: table has at least one column.
29#[derive(Debug, Clone, Copy)]
30pub struct HasColumns;
31
32// =============================================================================
33// CreateTableBuilder
34// =============================================================================
35
36/// Type-safe CREATE TABLE builder.
37///
38/// Uses the typestate pattern to ensure that:
39/// - A table name must be set before building
40/// - At least one column must be added before building
41///
42/// # Example
43///
44/// ```rust
45/// use oxide_sql_core::migrations::{CreateTableBuilder, bigint, varchar, timestamp};
46///
47/// let op = CreateTableBuilder::new()
48///     .name("users")
49///     .column(bigint("id").primary_key().autoincrement().build())
50///     .column(varchar("username", 255).not_null().unique().build())
51///     .column(timestamp("created_at").not_null().default_expr("CURRENT_TIMESTAMP").build())
52///     .build();
53///
54/// assert_eq!(op.name, "users");
55/// assert_eq!(op.columns.len(), 3);
56/// ```
57#[derive(Debug, Clone)]
58pub struct CreateTableBuilder<Name, Cols> {
59    name: Option<String>,
60    columns: Vec<ColumnDefinition>,
61    constraints: Vec<TableConstraint>,
62    if_not_exists: bool,
63    _state: PhantomData<(Name, Cols)>,
64}
65
66impl Default for CreateTableBuilder<NoName, NoColumns> {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl CreateTableBuilder<NoName, NoColumns> {
73    /// Creates a new `CreateTableBuilder`.
74    #[must_use]
75    pub fn new() -> Self {
76        Self {
77            name: None,
78            columns: Vec::new(),
79            constraints: Vec::new(),
80            if_not_exists: false,
81            _state: PhantomData,
82        }
83    }
84}
85
86impl<Cols> CreateTableBuilder<NoName, Cols> {
87    /// Sets the table name.
88    #[must_use]
89    pub fn name(self, name: impl Into<String>) -> CreateTableBuilder<HasName, Cols> {
90        CreateTableBuilder {
91            name: Some(name.into()),
92            columns: self.columns,
93            constraints: self.constraints,
94            if_not_exists: self.if_not_exists,
95            _state: PhantomData,
96        }
97    }
98}
99
100impl<Name> CreateTableBuilder<Name, NoColumns> {
101    /// Adds the first column to the table.
102    #[must_use]
103    pub fn column(self, column: ColumnDefinition) -> CreateTableBuilder<Name, HasColumns> {
104        CreateTableBuilder {
105            name: self.name,
106            columns: vec![column],
107            constraints: self.constraints,
108            if_not_exists: self.if_not_exists,
109            _state: PhantomData,
110        }
111    }
112}
113
114impl<Name> CreateTableBuilder<Name, HasColumns> {
115    /// Adds another column to the table.
116    #[must_use]
117    pub fn column(mut self, column: ColumnDefinition) -> Self {
118        self.columns.push(column);
119        self
120    }
121}
122
123impl<Name, Cols> CreateTableBuilder<Name, Cols> {
124    /// Uses IF NOT EXISTS clause.
125    #[must_use]
126    pub fn if_not_exists(mut self) -> Self {
127        self.if_not_exists = true;
128        self
129    }
130}
131
132impl<Cols> CreateTableBuilder<HasName, Cols> {
133    /// Adds a table-level constraint.
134    #[must_use]
135    pub fn constraint(mut self, constraint: TableConstraint) -> Self {
136        self.constraints.push(constraint);
137        self
138    }
139
140    /// Adds a composite primary key constraint.
141    #[must_use]
142    pub fn primary_key(mut self, columns: &[&str]) -> Self {
143        self.constraints.push(TableConstraint::PrimaryKey {
144            name: None,
145            columns: columns.iter().map(|&s| s.to_string()).collect(),
146        });
147        self
148    }
149
150    /// Adds a named composite primary key constraint.
151    #[must_use]
152    pub fn primary_key_named(mut self, name: impl Into<String>, columns: &[&str]) -> Self {
153        self.constraints.push(TableConstraint::PrimaryKey {
154            name: Some(name.into()),
155            columns: columns.iter().map(|&s| s.to_string()).collect(),
156        });
157        self
158    }
159
160    /// Adds a unique constraint on multiple columns.
161    #[must_use]
162    pub fn unique_constraint(mut self, columns: &[&str]) -> Self {
163        self.constraints.push(TableConstraint::Unique {
164            name: None,
165            columns: columns.iter().map(|&s| s.to_string()).collect(),
166        });
167        self
168    }
169
170    /// Adds a named unique constraint on multiple columns.
171    #[must_use]
172    pub fn unique_constraint_named(mut self, name: impl Into<String>, columns: &[&str]) -> Self {
173        self.constraints.push(TableConstraint::Unique {
174            name: Some(name.into()),
175            columns: columns.iter().map(|&s| s.to_string()).collect(),
176        });
177        self
178    }
179
180    /// Adds a check constraint.
181    #[must_use]
182    pub fn check_constraint(mut self, expression: impl Into<String>) -> Self {
183        self.constraints.push(TableConstraint::Check {
184            name: None,
185            expression: expression.into(),
186        });
187        self
188    }
189
190    /// Adds a named check constraint.
191    #[must_use]
192    pub fn check_constraint_named(
193        mut self,
194        name: impl Into<String>,
195        expression: impl Into<String>,
196    ) -> Self {
197        self.constraints.push(TableConstraint::Check {
198            name: Some(name.into()),
199            expression: expression.into(),
200        });
201        self
202    }
203}
204
205impl CreateTableBuilder<HasName, HasColumns> {
206    /// Builds the `CreateTableOp`.
207    #[must_use]
208    pub fn build(self) -> CreateTableOp {
209        CreateTableOp {
210            name: self.name.expect("Name was set"),
211            columns: self.columns,
212            constraints: self.constraints,
213            if_not_exists: self.if_not_exists,
214        }
215    }
216}
217
218// =============================================================================
219// DropTableBuilder
220// =============================================================================
221
222/// Builder for DROP TABLE operations.
223#[derive(Debug, Clone, Default)]
224pub struct DropTableBuilder {
225    name: Option<String>,
226    if_exists: bool,
227    cascade: bool,
228}
229
230impl DropTableBuilder {
231    /// Creates a new `DropTableBuilder`.
232    #[must_use]
233    pub fn new() -> Self {
234        Self::default()
235    }
236
237    /// Sets the table name.
238    #[must_use]
239    pub fn name(mut self, name: impl Into<String>) -> Self {
240        self.name = Some(name.into());
241        self
242    }
243
244    /// Uses IF EXISTS clause.
245    #[must_use]
246    pub fn if_exists(mut self) -> Self {
247        self.if_exists = true;
248        self
249    }
250
251    /// Uses CASCADE (PostgreSQL).
252    #[must_use]
253    pub fn cascade(mut self) -> Self {
254        self.cascade = true;
255        self
256    }
257
258    /// Builds the `DropTableOp`.
259    ///
260    /// # Panics
261    ///
262    /// Panics if no table name was set.
263    #[must_use]
264    pub fn build(self) -> DropTableOp {
265        DropTableOp {
266            name: self.name.expect("Table name must be set"),
267            if_exists: self.if_exists,
268            cascade: self.cascade,
269        }
270    }
271}
272
273// =============================================================================
274// IndexBuilder
275// =============================================================================
276
277use super::operation::{CreateIndexOp, IndexType};
278
279/// Builder for CREATE INDEX operations.
280#[derive(Debug, Clone, Default)]
281#[allow(dead_code)]
282pub struct CreateIndexBuilder {
283    name: Option<String>,
284    table: Option<String>,
285    columns: Vec<String>,
286    unique: bool,
287    index_type: IndexType,
288    if_not_exists: bool,
289    condition: Option<String>,
290}
291
292#[allow(dead_code)]
293impl CreateIndexBuilder {
294    /// Creates a new `CreateIndexBuilder`.
295    #[must_use]
296    pub fn new() -> Self {
297        Self::default()
298    }
299
300    /// Sets the index name.
301    #[must_use]
302    pub fn name(mut self, name: impl Into<String>) -> Self {
303        self.name = Some(name.into());
304        self
305    }
306
307    /// Sets the table name.
308    #[must_use]
309    pub fn on_table(mut self, table: impl Into<String>) -> Self {
310        self.table = Some(table.into());
311        self
312    }
313
314    /// Adds a column to the index.
315    #[must_use]
316    pub fn column(mut self, column: impl Into<String>) -> Self {
317        self.columns.push(column.into());
318        self
319    }
320
321    /// Adds multiple columns to the index.
322    #[must_use]
323    pub fn columns(mut self, columns: &[&str]) -> Self {
324        self.columns.extend(columns.iter().map(|&s| s.to_string()));
325        self
326    }
327
328    /// Makes this a unique index.
329    #[must_use]
330    pub fn unique(mut self) -> Self {
331        self.unique = true;
332        self
333    }
334
335    /// Sets the index type.
336    #[must_use]
337    pub fn index_type(mut self, index_type: IndexType) -> Self {
338        self.index_type = index_type;
339        self
340    }
341
342    /// Uses IF NOT EXISTS clause.
343    #[must_use]
344    pub fn if_not_exists(mut self) -> Self {
345        self.if_not_exists = true;
346        self
347    }
348
349    /// Adds a partial index condition (WHERE clause).
350    #[must_use]
351    pub fn where_clause(mut self, condition: impl Into<String>) -> Self {
352        self.condition = Some(condition.into());
353        self
354    }
355
356    /// Builds the `CreateIndexOp`.
357    ///
358    /// # Panics
359    ///
360    /// Panics if name, table, or columns are not set.
361    #[must_use]
362    pub fn build(self) -> CreateIndexOp {
363        CreateIndexOp {
364            name: self.name.expect("Index name must be set"),
365            table: self.table.expect("Table name must be set"),
366            columns: self.columns,
367            unique: self.unique,
368            index_type: self.index_type,
369            if_not_exists: self.if_not_exists,
370            condition: self.condition,
371        }
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use crate::migrations::column_builder::{bigint, boolean, timestamp, varchar};
379
380    #[test]
381    fn test_create_table_builder() {
382        let op = CreateTableBuilder::new()
383            .name("users")
384            .column(bigint("id").primary_key().autoincrement().build())
385            .column(varchar("username", 255).not_null().unique().build())
386            .column(varchar("email", 255).build())
387            .column(
388                timestamp("created_at")
389                    .not_null()
390                    .default_expr("CURRENT_TIMESTAMP")
391                    .build(),
392            )
393            .build();
394
395        assert_eq!(op.name, "users");
396        assert_eq!(op.columns.len(), 4);
397        assert!(!op.if_not_exists);
398
399        // Check first column
400        let id_col = &op.columns[0];
401        assert_eq!(id_col.name, "id");
402        assert!(id_col.primary_key);
403        assert!(id_col.autoincrement);
404    }
405
406    #[test]
407    fn test_create_table_if_not_exists() {
408        let op = CreateTableBuilder::new()
409            .if_not_exists()
410            .name("users")
411            .column(bigint("id").primary_key().build())
412            .build();
413
414        assert!(op.if_not_exists);
415    }
416
417    #[test]
418    fn test_create_table_with_constraints() {
419        let op = CreateTableBuilder::new()
420            .name("order_items")
421            .column(bigint("order_id").not_null().build())
422            .column(bigint("product_id").not_null().build())
423            .column(bigint("quantity").not_null().build())
424            .primary_key(&["order_id", "product_id"])
425            .unique_constraint(&["order_id", "product_id"])
426            .check_constraint("quantity > 0")
427            .build();
428
429        assert_eq!(op.constraints.len(), 3);
430
431        match &op.constraints[0] {
432            TableConstraint::PrimaryKey { columns, .. } => {
433                assert_eq!(columns, &["order_id", "product_id"]);
434            }
435            _ => panic!("Expected PrimaryKey constraint"),
436        }
437
438        match &op.constraints[1] {
439            TableConstraint::Unique { columns, .. } => {
440                assert_eq!(columns, &["order_id", "product_id"]);
441            }
442            _ => panic!("Expected Unique constraint"),
443        }
444
445        match &op.constraints[2] {
446            TableConstraint::Check { expression, .. } => {
447                assert_eq!(expression, "quantity > 0");
448            }
449            _ => panic!("Expected Check constraint"),
450        }
451    }
452
453    #[test]
454    fn test_drop_table_builder() {
455        let op = DropTableBuilder::new().name("users").build();
456        assert_eq!(op.name, "users");
457        assert!(!op.if_exists);
458        assert!(!op.cascade);
459
460        let op = DropTableBuilder::new()
461            .name("users")
462            .if_exists()
463            .cascade()
464            .build();
465        assert!(op.if_exists);
466        assert!(op.cascade);
467    }
468
469    #[test]
470    fn test_create_index_builder() {
471        let op = CreateIndexBuilder::new()
472            .name("idx_users_email")
473            .on_table("users")
474            .column("email")
475            .unique()
476            .build();
477
478        assert_eq!(op.name, "idx_users_email");
479        assert_eq!(op.table, "users");
480        assert_eq!(op.columns, vec!["email"]);
481        assert!(op.unique);
482    }
483
484    #[test]
485    fn test_create_composite_index() {
486        let op = CreateIndexBuilder::new()
487            .name("idx_invoices_company_status")
488            .on_table("invoices")
489            .columns(&["company_id", "status"])
490            .if_not_exists()
491            .build();
492
493        assert_eq!(op.columns, vec!["company_id", "status"]);
494        assert!(op.if_not_exists);
495    }
496
497    #[test]
498    fn test_partial_index() {
499        let op = CreateIndexBuilder::new()
500            .name("idx_active_users")
501            .on_table("users")
502            .column("email")
503            .where_clause("active = true")
504            .build();
505
506        assert_eq!(op.condition, Some("active = true".to_string()));
507    }
508
509    #[test]
510    fn test_fluent_api_order() {
511        // Verify we can chain methods in different orders
512        let op1 = CreateTableBuilder::new()
513            .name("test")
514            .column(boolean("flag").build())
515            .if_not_exists()
516            .build();
517
518        let op2 = CreateTableBuilder::new()
519            .if_not_exists()
520            .name("test")
521            .column(boolean("flag").build())
522            .build();
523
524        assert_eq!(op1.name, op2.name);
525        assert_eq!(op1.if_not_exists, op2.if_not_exists);
526    }
527}