1use std::borrow::Cow;
36
37use serde::{Deserialize, Serialize};
38
39use crate::error::{QueryError, QueryResult};
40use crate::sql::DatabaseType;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub enum PartitionType {
45 Range,
47 List,
49 Hash,
51}
52
53impl PartitionType {
54 pub fn to_postgres_sql(&self) -> &'static str {
56 match self {
57 Self::Range => "RANGE",
58 Self::List => "LIST",
59 Self::Hash => "HASH",
60 }
61 }
62
63 pub fn to_mysql_sql(&self) -> &'static str {
65 match self {
66 Self::Range => "RANGE",
67 Self::List => "LIST",
68 Self::Hash => "HASH",
69 }
70 }
71
72 pub fn to_mssql_sql(&self) -> &'static str {
74 match self {
75 Self::Range => "RANGE",
76 Self::List => "LIST",
77 Self::Hash => "HASH",
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub enum RangeBound {
85 MinValue,
87 MaxValue,
89 Value(String),
91 Date(String),
93 Int(i64),
95}
96
97impl RangeBound {
98 pub fn value(v: impl Into<String>) -> Self {
100 Self::Value(v.into())
101 }
102
103 pub fn date(d: impl Into<String>) -> Self {
105 Self::Date(d.into())
106 }
107
108 pub fn int(i: i64) -> Self {
110 Self::Int(i)
111 }
112
113 pub fn to_sql(&self) -> Cow<'static, str> {
115 match self {
116 Self::MinValue => Cow::Borrowed("MINVALUE"),
117 Self::MaxValue => Cow::Borrowed("MAXVALUE"),
118 Self::Value(v) => Cow::Owned(format!("'{}'", v)),
119 Self::Date(d) => Cow::Owned(format!("'{}'", d)),
120 Self::Int(i) => Cow::Owned(i.to_string()),
121 }
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct RangePartitionDef {
128 pub name: String,
130 pub from: RangeBound,
132 pub to: RangeBound,
134 pub tablespace: Option<String>,
136}
137
138impl RangePartitionDef {
139 pub fn new(
141 name: impl Into<String>,
142 from: RangeBound,
143 to: RangeBound,
144 ) -> Self {
145 Self {
146 name: name.into(),
147 from,
148 to,
149 tablespace: None,
150 }
151 }
152
153 pub fn tablespace(mut self, tablespace: impl Into<String>) -> Self {
155 self.tablespace = Some(tablespace.into());
156 self
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub struct ListPartitionDef {
163 pub name: String,
165 pub values: Vec<String>,
167 pub tablespace: Option<String>,
169}
170
171impl ListPartitionDef {
172 pub fn new(name: impl Into<String>, values: impl IntoIterator<Item = impl Into<String>>) -> Self {
174 Self {
175 name: name.into(),
176 values: values.into_iter().map(Into::into).collect(),
177 tablespace: None,
178 }
179 }
180
181 pub fn tablespace(mut self, tablespace: impl Into<String>) -> Self {
183 self.tablespace = Some(tablespace.into());
184 self
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub struct HashPartitionDef {
191 pub name: String,
193 pub modulus: u32,
195 pub remainder: u32,
197 pub tablespace: Option<String>,
199}
200
201impl HashPartitionDef {
202 pub fn new(name: impl Into<String>, modulus: u32, remainder: u32) -> Self {
204 Self {
205 name: name.into(),
206 modulus,
207 remainder,
208 tablespace: None,
209 }
210 }
211
212 pub fn tablespace(mut self, tablespace: impl Into<String>) -> Self {
214 self.tablespace = Some(tablespace.into());
215 self
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221pub enum PartitionDef {
222 Range(Vec<RangePartitionDef>),
224 List(Vec<ListPartitionDef>),
226 Hash(Vec<HashPartitionDef>),
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct Partition {
233 pub table: String,
235 pub schema: Option<String>,
237 pub partition_type: PartitionType,
239 pub columns: Vec<String>,
241 pub partitions: PartitionDef,
243 pub comment: Option<String>,
245}
246
247impl Partition {
248 pub fn builder(table: impl Into<String>) -> PartitionBuilder {
250 PartitionBuilder::new(table)
251 }
252
253 pub fn qualified_table(&self) -> Cow<'_, str> {
255 match &self.schema {
256 Some(schema) => Cow::Owned(format!("{}.{}", schema, self.table)),
257 None => Cow::Borrowed(&self.table),
258 }
259 }
260
261 pub fn to_postgres_partition_clause(&self) -> String {
263 format!(
264 "PARTITION BY {} ({})",
265 self.partition_type.to_postgres_sql(),
266 self.columns.join(", ")
267 )
268 }
269
270 pub fn to_postgres_create_partition(&self, def: &RangePartitionDef) -> String {
272 let mut sql = format!(
273 "CREATE TABLE {} PARTITION OF {}\n FOR VALUES FROM ({}) TO ({})",
274 def.name,
275 self.qualified_table(),
276 def.from.to_sql(),
277 def.to.to_sql()
278 );
279
280 if let Some(ref ts) = def.tablespace {
281 sql.push_str(&format!("\n TABLESPACE {}", ts));
282 }
283
284 sql.push(';');
285 sql
286 }
287
288 pub fn to_postgres_create_list_partition(&self, def: &ListPartitionDef) -> String {
290 let values: Vec<String> = def.values.iter().map(|v| format!("'{}'", v)).collect();
291
292 let mut sql = format!(
293 "CREATE TABLE {} PARTITION OF {}\n FOR VALUES IN ({})",
294 def.name,
295 self.qualified_table(),
296 values.join(", ")
297 );
298
299 if let Some(ref ts) = def.tablespace {
300 sql.push_str(&format!("\n TABLESPACE {}", ts));
301 }
302
303 sql.push(';');
304 sql
305 }
306
307 pub fn to_postgres_create_hash_partition(&self, def: &HashPartitionDef) -> String {
309 let mut sql = format!(
310 "CREATE TABLE {} PARTITION OF {}\n FOR VALUES WITH (MODULUS {}, REMAINDER {})",
311 def.name,
312 self.qualified_table(),
313 def.modulus,
314 def.remainder
315 );
316
317 if let Some(ref ts) = def.tablespace {
318 sql.push_str(&format!("\n TABLESPACE {}", ts));
319 }
320
321 sql.push(';');
322 sql
323 }
324
325 pub fn to_postgres_create_all_partitions(&self) -> Vec<String> {
327 match &self.partitions {
328 PartitionDef::Range(ranges) => ranges
329 .iter()
330 .map(|r| self.to_postgres_create_partition(r))
331 .collect(),
332 PartitionDef::List(lists) => lists
333 .iter()
334 .map(|l| self.to_postgres_create_list_partition(l))
335 .collect(),
336 PartitionDef::Hash(hashes) => hashes
337 .iter()
338 .map(|h| self.to_postgres_create_hash_partition(h))
339 .collect(),
340 }
341 }
342
343 pub fn to_mysql_partition_clause(&self) -> String {
345 let columns_expr = if self.columns.len() == 1 {
346 self.columns[0].clone()
347 } else {
348 format!("({})", self.columns.join(", "))
349 };
350
351 let mut sql = format!(
352 "PARTITION BY {} ({})",
353 self.partition_type.to_mysql_sql(),
354 columns_expr
355 );
356
357 match &self.partitions {
359 PartitionDef::Range(ranges) => {
360 sql.push_str(" (\n");
361 for (i, r) in ranges.iter().enumerate() {
362 if i > 0 {
363 sql.push_str(",\n");
364 }
365 sql.push_str(&format!(
366 " PARTITION {} VALUES LESS THAN ({})",
367 r.name,
368 r.to.to_sql()
369 ));
370 }
371 sql.push_str("\n)");
372 }
373 PartitionDef::List(lists) => {
374 sql.push_str(" (\n");
375 for (i, l) in lists.iter().enumerate() {
376 if i > 0 {
377 sql.push_str(",\n");
378 }
379 let values: Vec<String> = l.values.iter().map(|v| format!("'{}'", v)).collect();
380 sql.push_str(&format!(
381 " PARTITION {} VALUES IN ({})",
382 l.name,
383 values.join(", ")
384 ));
385 }
386 sql.push_str("\n)");
387 }
388 PartitionDef::Hash(hashes) => {
389 sql.push_str(&format!(" PARTITIONS {}", hashes.len()));
390 }
391 }
392
393 sql
394 }
395
396 pub fn to_mssql_partition_sql(&self) -> QueryResult<Vec<String>> {
398 match &self.partitions {
399 PartitionDef::Range(ranges) => {
400 let mut sqls = Vec::new();
401
402 let boundaries: Vec<String> = ranges
404 .iter()
405 .filter(|r| !matches!(r.to, RangeBound::MaxValue))
406 .map(|r| r.to.to_sql().into_owned())
407 .collect();
408
409 let func_name = format!("{}_pf", self.table);
410 sqls.push(format!(
411 "CREATE PARTITION FUNCTION {}(datetime2)\nAS RANGE RIGHT FOR VALUES ({});",
412 func_name,
413 boundaries.join(", ")
414 ));
415
416 let scheme_name = format!("{}_ps", self.table);
418 let filegroups: Vec<String> = ranges.iter().map(|_| "PRIMARY".to_string()).collect();
419 sqls.push(format!(
420 "CREATE PARTITION SCHEME {}\nAS PARTITION {}\nTO ({});",
421 scheme_name,
422 func_name,
423 filegroups.join(", ")
424 ));
425
426 Ok(sqls)
427 }
428 PartitionDef::List(_) => Err(QueryError::unsupported(
429 "MSSQL uses partition functions differently for list partitioning. Consider using range partitioning.",
430 )),
431 PartitionDef::Hash(_) => Err(QueryError::unsupported(
432 "MSSQL does not directly support hash partitioning. Use a computed column with range partitioning.",
433 )),
434 }
435 }
436
437 pub fn attach_partition_sql(
439 &self,
440 partition_name: &str,
441 db_type: DatabaseType,
442 ) -> QueryResult<String> {
443 match db_type {
444 DatabaseType::PostgreSQL => Ok(format!(
445 "ALTER TABLE {} ATTACH PARTITION {};",
446 self.qualified_table(),
447 partition_name
448 )),
449 DatabaseType::MySQL => Err(QueryError::unsupported(
450 "MySQL does not support ATTACH PARTITION. Use ALTER TABLE ... REORGANIZE PARTITION.",
451 )),
452 DatabaseType::SQLite => Err(QueryError::unsupported(
453 "SQLite does not support table partitioning.",
454 )),
455 DatabaseType::MSSQL => Err(QueryError::unsupported(
456 "MSSQL uses SWITCH to move partitions. Use partition switching instead.",
457 )),
458 }
459 }
460
461 pub fn detach_partition_sql(
463 &self,
464 partition_name: &str,
465 db_type: DatabaseType,
466 ) -> QueryResult<String> {
467 match db_type {
468 DatabaseType::PostgreSQL => Ok(format!(
469 "ALTER TABLE {} DETACH PARTITION {};",
470 self.qualified_table(),
471 partition_name
472 )),
473 DatabaseType::MySQL => Err(QueryError::unsupported(
474 "MySQL does not support DETACH PARTITION. Drop and recreate the partition.",
475 )),
476 DatabaseType::SQLite => Err(QueryError::unsupported(
477 "SQLite does not support table partitioning.",
478 )),
479 DatabaseType::MSSQL => Err(QueryError::unsupported(
480 "MSSQL uses SWITCH to move partitions. Use partition switching instead.",
481 )),
482 }
483 }
484
485 pub fn drop_partition_sql(
487 &self,
488 partition_name: &str,
489 db_type: DatabaseType,
490 ) -> QueryResult<String> {
491 match db_type {
492 DatabaseType::PostgreSQL => Ok(format!("DROP TABLE IF EXISTS {};", partition_name)),
493 DatabaseType::MySQL => Ok(format!(
494 "ALTER TABLE {} DROP PARTITION {};",
495 self.qualified_table(),
496 partition_name
497 )),
498 DatabaseType::SQLite => Err(QueryError::unsupported(
499 "SQLite does not support table partitioning.",
500 )),
501 DatabaseType::MSSQL => Ok(format!(
502 "ALTER TABLE {} DROP PARTITION {};",
503 self.qualified_table(),
504 partition_name
505 )),
506 }
507 }
508}
509
510#[derive(Debug, Clone)]
512pub struct PartitionBuilder {
513 table: String,
514 schema: Option<String>,
515 partition_type: Option<PartitionType>,
516 columns: Vec<String>,
517 range_partitions: Vec<RangePartitionDef>,
518 list_partitions: Vec<ListPartitionDef>,
519 hash_partitions: Vec<HashPartitionDef>,
520 comment: Option<String>,
521}
522
523impl PartitionBuilder {
524 pub fn new(table: impl Into<String>) -> Self {
526 Self {
527 table: table.into(),
528 schema: None,
529 partition_type: None,
530 columns: Vec::new(),
531 range_partitions: Vec::new(),
532 list_partitions: Vec::new(),
533 hash_partitions: Vec::new(),
534 comment: None,
535 }
536 }
537
538 pub fn schema(mut self, schema: impl Into<String>) -> Self {
540 self.schema = Some(schema.into());
541 self
542 }
543
544 pub fn range_partition(mut self) -> Self {
546 self.partition_type = Some(PartitionType::Range);
547 self
548 }
549
550 pub fn list_partition(mut self) -> Self {
552 self.partition_type = Some(PartitionType::List);
553 self
554 }
555
556 pub fn hash_partition(mut self) -> Self {
558 self.partition_type = Some(PartitionType::Hash);
559 self
560 }
561
562 pub fn column(mut self, column: impl Into<String>) -> Self {
564 self.columns.push(column.into());
565 self
566 }
567
568 pub fn columns(mut self, columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
570 self.columns.extend(columns.into_iter().map(Into::into));
571 self
572 }
573
574 pub fn add_range(
576 mut self,
577 name: impl Into<String>,
578 from: RangeBound,
579 to: RangeBound,
580 ) -> Self {
581 self.range_partitions.push(RangePartitionDef::new(name, from, to));
582 self
583 }
584
585 pub fn add_range_with_tablespace(
587 mut self,
588 name: impl Into<String>,
589 from: RangeBound,
590 to: RangeBound,
591 tablespace: impl Into<String>,
592 ) -> Self {
593 self.range_partitions.push(
594 RangePartitionDef::new(name, from, to).tablespace(tablespace),
595 );
596 self
597 }
598
599 pub fn add_list(
601 mut self,
602 name: impl Into<String>,
603 values: impl IntoIterator<Item = impl Into<String>>,
604 ) -> Self {
605 self.list_partitions.push(ListPartitionDef::new(name, values));
606 self
607 }
608
609 pub fn add_hash(mut self, name: impl Into<String>, modulus: u32, remainder: u32) -> Self {
611 self.hash_partitions.push(HashPartitionDef::new(name, modulus, remainder));
612 self
613 }
614
615 pub fn add_hash_partitions(mut self, count: u32, name_prefix: impl Into<String>) -> Self {
617 let prefix = name_prefix.into();
618 for i in 0..count {
619 self.hash_partitions.push(HashPartitionDef::new(
620 format!("{}_{}", prefix, i),
621 count,
622 i,
623 ));
624 }
625 self
626 }
627
628 pub fn comment(mut self, comment: impl Into<String>) -> Self {
630 self.comment = Some(comment.into());
631 self
632 }
633
634 pub fn build(self) -> QueryResult<Partition> {
636 let partition_type = self.partition_type.ok_or_else(|| {
637 QueryError::invalid_input(
638 "partition_type",
639 "Must specify partition type (range_partition, list_partition, or hash_partition)",
640 )
641 })?;
642
643 if self.columns.is_empty() {
644 return Err(QueryError::invalid_input(
645 "columns",
646 "Must specify at least one partition column",
647 ));
648 }
649
650 let partitions = match partition_type {
651 PartitionType::Range => {
652 if self.range_partitions.is_empty() {
653 return Err(QueryError::invalid_input(
654 "partitions",
655 "Must define at least one range partition with add_range()",
656 ));
657 }
658 PartitionDef::Range(self.range_partitions)
659 }
660 PartitionType::List => {
661 if self.list_partitions.is_empty() {
662 return Err(QueryError::invalid_input(
663 "partitions",
664 "Must define at least one list partition with add_list()",
665 ));
666 }
667 PartitionDef::List(self.list_partitions)
668 }
669 PartitionType::Hash => {
670 if self.hash_partitions.is_empty() {
671 return Err(QueryError::invalid_input(
672 "partitions",
673 "Must define at least one hash partition with add_hash() or add_hash_partitions()",
674 ));
675 }
676 PartitionDef::Hash(self.hash_partitions)
677 }
678 };
679
680 Ok(Partition {
681 table: self.table,
682 schema: self.schema,
683 partition_type,
684 columns: self.columns,
685 partitions,
686 comment: self.comment,
687 })
688 }
689}
690
691pub mod time_partitions {
693 use super::*;
694
695 pub fn monthly_partitions(
697 table: &str,
698 column: &str,
699 start_year: i32,
700 start_month: u32,
701 count: u32,
702 ) -> PartitionBuilder {
703 let mut builder = Partition::builder(table)
704 .range_partition()
705 .column(column);
706
707 let mut year = start_year;
708 let mut month = start_month;
709
710 for _ in 0..count {
711 let from_date = format!("{:04}-{:02}-01", year, month);
712
713 let (next_year, next_month) = if month == 12 {
715 (year + 1, 1)
716 } else {
717 (year, month + 1)
718 };
719 let to_date = format!("{:04}-{:02}-01", next_year, next_month);
720
721 let partition_name = format!("{}_{:04}_{:02}", table, year, month);
722
723 builder = builder.add_range(
724 partition_name,
725 RangeBound::date(from_date),
726 RangeBound::date(to_date),
727 );
728
729 year = next_year;
730 month = next_month;
731 }
732
733 builder
734 }
735
736 pub fn quarterly_partitions(
738 table: &str,
739 column: &str,
740 start_year: i32,
741 count: u32,
742 ) -> PartitionBuilder {
743 let mut builder = Partition::builder(table)
744 .range_partition()
745 .column(column);
746
747 let mut year = start_year;
748 let mut quarter = 1;
749
750 for _ in 0..count {
751 let from_month = (quarter - 1) * 3 + 1;
752 let from_date = format!("{:04}-{:02}-01", year, from_month);
753
754 let (next_year, next_quarter) = if quarter == 4 {
755 (year + 1, 1)
756 } else {
757 (year, quarter + 1)
758 };
759 let to_month = (next_quarter - 1) * 3 + 1;
760 let to_date = format!("{:04}-{:02}-01", next_year, to_month);
761
762 let partition_name = format!("{}_{}q{}", table, year, quarter);
763
764 builder = builder.add_range(
765 partition_name,
766 RangeBound::date(from_date),
767 RangeBound::date(to_date),
768 );
769
770 year = next_year;
771 quarter = next_quarter;
772 }
773
774 builder
775 }
776
777 pub fn yearly_partitions(
779 table: &str,
780 column: &str,
781 start_year: i32,
782 count: u32,
783 ) -> PartitionBuilder {
784 let mut builder = Partition::builder(table)
785 .range_partition()
786 .column(column);
787
788 for i in 0..count {
789 let year = start_year + i as i32;
790 let from_date = format!("{:04}-01-01", year);
791 let to_date = format!("{:04}-01-01", year + 1);
792 let partition_name = format!("{}_{}", table, year);
793
794 builder = builder.add_range(
795 partition_name,
796 RangeBound::date(from_date),
797 RangeBound::date(to_date),
798 );
799 }
800
801 builder
802 }
803}
804
805pub mod mongodb {
807 use serde::{Deserialize, Serialize};
808
809 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
811 pub enum ShardKeyType {
812 Range,
814 Hashed,
816 }
817
818 impl ShardKeyType {
819 pub fn as_index_value(&self) -> serde_json::Value {
821 match self {
822 Self::Range => serde_json::json!(1),
823 Self::Hashed => serde_json::json!("hashed"),
824 }
825 }
826 }
827
828 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
830 pub struct ShardKey {
831 pub fields: Vec<(String, ShardKeyType)>,
833 pub unique: bool,
835 }
836
837 impl ShardKey {
838 pub fn builder() -> ShardKeyBuilder {
840 ShardKeyBuilder::new()
841 }
842
843 pub fn shard_collection_command(
845 &self,
846 database: &str,
847 collection: &str,
848 ) -> serde_json::Value {
849 let mut key = serde_json::Map::new();
850 for (field, key_type) in &self.fields {
851 key.insert(field.clone(), key_type.as_index_value());
852 }
853
854 serde_json::json!({
855 "shardCollection": format!("{}.{}", database, collection),
856 "key": key,
857 "unique": self.unique
858 })
859 }
860
861 pub fn index_spec(&self) -> serde_json::Value {
863 let mut spec = serde_json::Map::new();
864 for (field, key_type) in &self.fields {
865 spec.insert(field.clone(), key_type.as_index_value());
866 }
867 serde_json::Value::Object(spec)
868 }
869 }
870
871 #[derive(Debug, Clone, Default)]
873 pub struct ShardKeyBuilder {
874 fields: Vec<(String, ShardKeyType)>,
875 unique: bool,
876 }
877
878 impl ShardKeyBuilder {
879 pub fn new() -> Self {
881 Self::default()
882 }
883
884 pub fn range_field(mut self, field: impl Into<String>) -> Self {
886 self.fields.push((field.into(), ShardKeyType::Range));
887 self
888 }
889
890 pub fn hashed_field(mut self, field: impl Into<String>) -> Self {
892 self.fields.push((field.into(), ShardKeyType::Hashed));
893 self
894 }
895
896 pub fn unique(mut self, unique: bool) -> Self {
898 self.unique = unique;
899 self
900 }
901
902 pub fn build(self) -> ShardKey {
904 ShardKey {
905 fields: self.fields,
906 unique: self.unique,
907 }
908 }
909 }
910
911 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
913 pub struct ShardZone {
914 pub name: String,
916 pub min: serde_json::Value,
918 pub max: serde_json::Value,
920 }
921
922 impl ShardZone {
923 pub fn new(
925 name: impl Into<String>,
926 min: serde_json::Value,
927 max: serde_json::Value,
928 ) -> Self {
929 Self {
930 name: name.into(),
931 min,
932 max,
933 }
934 }
935
936 pub fn update_zone_key_range_command(
938 &self,
939 namespace: &str,
940 ) -> serde_json::Value {
941 serde_json::json!({
942 "updateZoneKeyRange": namespace,
943 "min": self.min,
944 "max": self.max,
945 "zone": self.name
946 })
947 }
948
949 pub fn add_shard_to_zone_command(&self, shard: &str) -> serde_json::Value {
951 serde_json::json!({
952 "addShardToZone": shard,
953 "zone": self.name
954 })
955 }
956 }
957
958 #[derive(Debug, Clone, Default)]
960 pub struct ZoneShardingBuilder {
961 zones: Vec<ShardZone>,
962 shard_assignments: Vec<(String, String)>, }
964
965 impl ZoneShardingBuilder {
966 pub fn new() -> Self {
968 Self::default()
969 }
970
971 pub fn add_zone(
973 mut self,
974 name: impl Into<String>,
975 min: serde_json::Value,
976 max: serde_json::Value,
977 ) -> Self {
978 self.zones.push(ShardZone::new(name, min, max));
979 self
980 }
981
982 pub fn assign_shard(mut self, shard: impl Into<String>, zone: impl Into<String>) -> Self {
984 self.shard_assignments.push((shard.into(), zone.into()));
985 self
986 }
987
988 pub fn build_commands(&self, namespace: &str) -> Vec<serde_json::Value> {
990 let mut commands = Vec::new();
991
992 for (shard, zone) in &self.shard_assignments {
994 commands.push(serde_json::json!({
995 "addShardToZone": shard,
996 "zone": zone
997 }));
998 }
999
1000 for zone in &self.zones {
1002 commands.push(zone.update_zone_key_range_command(namespace));
1003 }
1004
1005 commands
1006 }
1007 }
1008
1009 pub fn shard_key() -> ShardKeyBuilder {
1011 ShardKeyBuilder::new()
1012 }
1013
1014 pub fn zone_sharding() -> ZoneShardingBuilder {
1016 ZoneShardingBuilder::new()
1017 }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::*;
1023
1024 #[test]
1025 fn test_range_partition_builder() {
1026 let partition = Partition::builder("orders")
1027 .schema("sales")
1028 .range_partition()
1029 .column("created_at")
1030 .add_range("orders_2024_q1", RangeBound::date("2024-01-01"), RangeBound::date("2024-04-01"))
1031 .add_range("orders_2024_q2", RangeBound::date("2024-04-01"), RangeBound::date("2024-07-01"))
1032 .build()
1033 .unwrap();
1034
1035 assert_eq!(partition.table, "orders");
1036 assert_eq!(partition.partition_type, PartitionType::Range);
1037 assert_eq!(partition.columns, vec!["created_at"]);
1038 }
1039
1040 #[test]
1041 fn test_postgres_partition_clause() {
1042 let partition = Partition::builder("orders")
1043 .range_partition()
1044 .column("created_at")
1045 .add_range("orders_2024", RangeBound::date("2024-01-01"), RangeBound::date("2025-01-01"))
1046 .build()
1047 .unwrap();
1048
1049 let clause = partition.to_postgres_partition_clause();
1050 assert_eq!(clause, "PARTITION BY RANGE (created_at)");
1051 }
1052
1053 #[test]
1054 fn test_postgres_create_partition() {
1055 let partition = Partition::builder("orders")
1056 .range_partition()
1057 .column("created_at")
1058 .add_range("orders_2024", RangeBound::date("2024-01-01"), RangeBound::date("2025-01-01"))
1059 .build()
1060 .unwrap();
1061
1062 let sqls = partition.to_postgres_create_all_partitions();
1063 assert_eq!(sqls.len(), 1);
1064 assert!(sqls[0].contains("CREATE TABLE orders_2024 PARTITION OF orders"));
1065 assert!(sqls[0].contains("FOR VALUES FROM ('2024-01-01') TO ('2025-01-01')"));
1066 }
1067
1068 #[test]
1069 fn test_list_partition() {
1070 let partition = Partition::builder("users")
1071 .list_partition()
1072 .column("country")
1073 .add_list("users_us", ["US", "USA"])
1074 .add_list("users_eu", ["DE", "FR", "GB", "IT"])
1075 .build()
1076 .unwrap();
1077
1078 assert_eq!(partition.partition_type, PartitionType::List);
1079
1080 let sqls = partition.to_postgres_create_all_partitions();
1081 assert_eq!(sqls.len(), 2);
1082 assert!(sqls[0].contains("FOR VALUES IN"));
1083 }
1084
1085 #[test]
1086 fn test_hash_partition() {
1087 let partition = Partition::builder("events")
1088 .hash_partition()
1089 .column("user_id")
1090 .add_hash_partitions(4, "events")
1091 .build()
1092 .unwrap();
1093
1094 assert_eq!(partition.partition_type, PartitionType::Hash);
1095
1096 let sqls = partition.to_postgres_create_all_partitions();
1097 assert_eq!(sqls.len(), 4);
1098 assert!(sqls[0].contains("MODULUS 4"));
1099 assert!(sqls[0].contains("REMAINDER 0"));
1100 }
1101
1102 #[test]
1103 fn test_mysql_partition_clause() {
1104 let partition = Partition::builder("orders")
1105 .range_partition()
1106 .column("created_at")
1107 .add_range("p2024", RangeBound::MinValue, RangeBound::date("2025-01-01"))
1108 .add_range("p_future", RangeBound::date("2025-01-01"), RangeBound::MaxValue)
1109 .build()
1110 .unwrap();
1111
1112 let clause = partition.to_mysql_partition_clause();
1113 assert!(clause.contains("PARTITION BY RANGE"));
1114 assert!(clause.contains("PARTITION p2024"));
1115 assert!(clause.contains("PARTITION p_future"));
1116 }
1117
1118 #[test]
1119 fn test_detach_partition() {
1120 let partition = Partition::builder("orders")
1121 .range_partition()
1122 .column("created_at")
1123 .add_range("orders_2024", RangeBound::date("2024-01-01"), RangeBound::date("2025-01-01"))
1124 .build()
1125 .unwrap();
1126
1127 let sql = partition.detach_partition_sql("orders_2024", DatabaseType::PostgreSQL).unwrap();
1128 assert_eq!(sql, "ALTER TABLE orders DETACH PARTITION orders_2024;");
1129 }
1130
1131 #[test]
1132 fn test_drop_partition() {
1133 let partition = Partition::builder("orders")
1134 .range_partition()
1135 .column("created_at")
1136 .add_range("orders_2024", RangeBound::date("2024-01-01"), RangeBound::date("2025-01-01"))
1137 .build()
1138 .unwrap();
1139
1140 let pg_sql = partition.drop_partition_sql("orders_2024", DatabaseType::PostgreSQL).unwrap();
1141 assert_eq!(pg_sql, "DROP TABLE IF EXISTS orders_2024;");
1142
1143 let mysql_sql = partition.drop_partition_sql("orders_2024", DatabaseType::MySQL).unwrap();
1144 assert!(mysql_sql.contains("DROP PARTITION"));
1145 }
1146
1147 #[test]
1148 fn test_missing_partition_type() {
1149 let result = Partition::builder("orders")
1150 .column("created_at")
1151 .add_range("p1", RangeBound::MinValue, RangeBound::MaxValue)
1152 .build();
1153
1154 assert!(result.is_err());
1155 }
1156
1157 #[test]
1158 fn test_missing_columns() {
1159 let result = Partition::builder("orders")
1160 .range_partition()
1161 .add_range("p1", RangeBound::MinValue, RangeBound::MaxValue)
1162 .build();
1163
1164 assert!(result.is_err());
1165 }
1166
1167 mod time_partition_tests {
1168 use super::super::time_partitions;
1169
1170 #[test]
1171 fn test_monthly_partitions() {
1172 let builder = time_partitions::monthly_partitions("orders", "created_at", 2024, 1, 3);
1173 let partition = builder.build().unwrap();
1174
1175 if let super::PartitionDef::Range(ranges) = &partition.partitions {
1176 assert_eq!(ranges.len(), 3);
1177 assert_eq!(ranges[0].name, "orders_2024_01");
1178 assert_eq!(ranges[1].name, "orders_2024_02");
1179 assert_eq!(ranges[2].name, "orders_2024_03");
1180 } else {
1181 panic!("Expected range partitions");
1182 }
1183 }
1184
1185 #[test]
1186 fn test_quarterly_partitions() {
1187 let builder = time_partitions::quarterly_partitions("sales", "order_date", 2024, 4);
1188 let partition = builder.build().unwrap();
1189
1190 if let super::PartitionDef::Range(ranges) = &partition.partitions {
1191 assert_eq!(ranges.len(), 4);
1192 assert_eq!(ranges[0].name, "sales_2024q1");
1193 assert_eq!(ranges[3].name, "sales_2024q4");
1194 } else {
1195 panic!("Expected range partitions");
1196 }
1197 }
1198
1199 #[test]
1200 fn test_yearly_partitions() {
1201 let builder = time_partitions::yearly_partitions("logs", "timestamp", 2020, 5);
1202 let partition = builder.build().unwrap();
1203
1204 if let super::PartitionDef::Range(ranges) = &partition.partitions {
1205 assert_eq!(ranges.len(), 5);
1206 assert_eq!(ranges[0].name, "logs_2020");
1207 assert_eq!(ranges[4].name, "logs_2024");
1208 } else {
1209 panic!("Expected range partitions");
1210 }
1211 }
1212 }
1213
1214 mod mongodb_tests {
1215 use super::super::mongodb::*;
1216
1217 #[test]
1218 fn test_shard_key_builder() {
1219 let key = shard_key()
1220 .hashed_field("tenant_id")
1221 .range_field("created_at")
1222 .unique(false)
1223 .build();
1224
1225 assert_eq!(key.fields.len(), 2);
1226 assert_eq!(key.fields[0], ("tenant_id".to_string(), ShardKeyType::Hashed));
1227 assert_eq!(key.fields[1], ("created_at".to_string(), ShardKeyType::Range));
1228 }
1229
1230 #[test]
1231 fn test_shard_collection_command() {
1232 let key = shard_key()
1233 .hashed_field("user_id")
1234 .build();
1235
1236 let cmd = key.shard_collection_command("mydb", "users");
1237 assert_eq!(cmd["shardCollection"], "mydb.users");
1238 assert_eq!(cmd["key"]["user_id"], "hashed");
1239 }
1240
1241 #[test]
1242 fn test_zone_sharding() {
1243 let builder = zone_sharding()
1244 .add_zone("US", serde_json::json!({"region": "US"}), serde_json::json!({"region": "US~"}))
1245 .add_zone("EU", serde_json::json!({"region": "EU"}), serde_json::json!({"region": "EU~"}))
1246 .assign_shard("shard0", "US")
1247 .assign_shard("shard1", "EU");
1248
1249 let commands = builder.build_commands("mydb.users");
1250 assert_eq!(commands.len(), 4); }
1252
1253 #[test]
1254 fn test_shard_key_index_spec() {
1255 let key = shard_key()
1256 .range_field("tenant_id")
1257 .range_field("created_at")
1258 .build();
1259
1260 let spec = key.index_spec();
1261 assert_eq!(spec["tenant_id"], 1);
1262 assert_eq!(spec["created_at"], 1);
1263 }
1264 }
1265}
1266
1267
1268
1269