oxide_sql_core/migrations/
table_builder.rs1use std::marker::PhantomData;
8
9use super::column_builder::ColumnDefinition;
10use super::operation::{CreateTableOp, DropTableOp, TableConstraint};
11
12#[derive(Debug, Clone, Copy)]
18pub struct NoName;
19
20#[derive(Debug, Clone, Copy)]
22pub struct HasName;
23
24#[derive(Debug, Clone, Copy)]
26pub struct NoColumns;
27
28#[derive(Debug, Clone, Copy)]
30pub struct HasColumns;
31
32#[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
135 pub fn constraint(mut self, constraint: TableConstraint) -> Self {
136 self.constraints.push(constraint);
137 self
138 }
139
140 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Default)]
224pub struct DropTableBuilder {
225 name: Option<String>,
226 if_exists: bool,
227 cascade: bool,
228}
229
230impl DropTableBuilder {
231 #[must_use]
233 pub fn new() -> Self {
234 Self::default()
235 }
236
237 #[must_use]
239 pub fn name(mut self, name: impl Into<String>) -> Self {
240 self.name = Some(name.into());
241 self
242 }
243
244 #[must_use]
246 pub fn if_exists(mut self) -> Self {
247 self.if_exists = true;
248 self
249 }
250
251 #[must_use]
253 pub fn cascade(mut self) -> Self {
254 self.cascade = true;
255 self
256 }
257
258 #[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
273use super::operation::{CreateIndexOp, IndexType};
278
279#[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 #[must_use]
296 pub fn new() -> Self {
297 Self::default()
298 }
299
300 #[must_use]
302 pub fn name(mut self, name: impl Into<String>) -> Self {
303 self.name = Some(name.into());
304 self
305 }
306
307 #[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 #[must_use]
316 pub fn column(mut self, column: impl Into<String>) -> Self {
317 self.columns.push(column.into());
318 self
319 }
320
321 #[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 #[must_use]
330 pub fn unique(mut self) -> Self {
331 self.unique = true;
332 self
333 }
334
335 #[must_use]
337 pub fn index_type(mut self, index_type: IndexType) -> Self {
338 self.index_type = index_type;
339 self
340 }
341
342 #[must_use]
344 pub fn if_not_exists(mut self) -> Self {
345 self.if_not_exists = true;
346 self
347 }
348
349 #[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 #[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 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 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}