Skip to main content

prax_schema/ast/
relation.rs

1//! Relation analysis for the Prax schema AST.
2
3use serde::{Deserialize, Serialize};
4use smol_str::SmolStr;
5
6use super::ReferentialAction;
7
8/// The type of relation between two models.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum RelationType {
11    /// One-to-one relation.
12    OneToOne,
13    /// One-to-many relation.
14    OneToMany,
15    /// Many-to-one relation (inverse of one-to-many).
16    ManyToOne,
17    /// Many-to-many relation.
18    ManyToMany,
19}
20
21impl RelationType {
22    /// Check if this is a "to-one" relation.
23    pub fn is_to_one(&self) -> bool {
24        matches!(self, Self::OneToOne | Self::ManyToOne)
25    }
26
27    /// Check if this is a "to-many" relation.
28    pub fn is_to_many(&self) -> bool {
29        matches!(self, Self::OneToMany | Self::ManyToMany)
30    }
31
32    /// Check if this is a "from-many" relation.
33    pub fn is_from_many(&self) -> bool {
34        matches!(self, Self::ManyToOne | Self::ManyToMany)
35    }
36}
37
38impl std::fmt::Display for RelationType {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            Self::OneToOne => write!(f, "1:1"),
42            Self::OneToMany => write!(f, "1:n"),
43            Self::ManyToOne => write!(f, "n:1"),
44            Self::ManyToMany => write!(f, "m:n"),
45        }
46    }
47}
48
49/// A resolved relation between two models.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct Relation {
52    /// Relation name (for disambiguation when multiple relations exist).
53    pub name: Option<SmolStr>,
54    /// The model containing the foreign key.
55    pub from_model: SmolStr,
56    /// The field on the from model.
57    pub from_field: SmolStr,
58    /// The foreign key field(s) on the from model.
59    pub from_fields: Vec<SmolStr>,
60    /// The model being referenced.
61    pub to_model: SmolStr,
62    /// The field on the to model (back-relation).
63    pub to_field: Option<SmolStr>,
64    /// The referenced field(s) on the to model.
65    pub to_fields: Vec<SmolStr>,
66    /// The type of relation.
67    pub relation_type: RelationType,
68    /// On delete action.
69    pub on_delete: Option<ReferentialAction>,
70    /// On update action.
71    pub on_update: Option<ReferentialAction>,
72    /// Custom foreign key constraint name in the database.
73    pub map: Option<SmolStr>,
74}
75
76impl Relation {
77    /// Create a new relation.
78    pub fn new(
79        from_model: impl Into<SmolStr>,
80        from_field: impl Into<SmolStr>,
81        to_model: impl Into<SmolStr>,
82        relation_type: RelationType,
83    ) -> Self {
84        Self {
85            name: None,
86            from_model: from_model.into(),
87            from_field: from_field.into(),
88            from_fields: vec![],
89            to_model: to_model.into(),
90            to_field: None,
91            to_fields: vec![],
92            relation_type,
93            on_delete: None,
94            on_update: None,
95            map: None,
96        }
97    }
98
99    /// Set the relation name.
100    pub fn with_name(mut self, name: impl Into<SmolStr>) -> Self {
101        self.name = Some(name.into());
102        self
103    }
104
105    /// Set the foreign key fields.
106    pub fn with_from_fields(mut self, fields: Vec<SmolStr>) -> Self {
107        self.from_fields = fields;
108        self
109    }
110
111    /// Set the referenced fields.
112    pub fn with_to_fields(mut self, fields: Vec<SmolStr>) -> Self {
113        self.to_fields = fields;
114        self
115    }
116
117    /// Set the back-relation field.
118    pub fn with_to_field(mut self, field: impl Into<SmolStr>) -> Self {
119        self.to_field = Some(field.into());
120        self
121    }
122
123    /// Set the on delete action.
124    pub fn with_on_delete(mut self, action: ReferentialAction) -> Self {
125        self.on_delete = Some(action);
126        self
127    }
128
129    /// Set the on update action.
130    pub fn with_on_update(mut self, action: ReferentialAction) -> Self {
131        self.on_update = Some(action);
132        self
133    }
134
135    /// Set the custom foreign key constraint name.
136    pub fn with_map(mut self, name: impl Into<SmolStr>) -> Self {
137        self.map = Some(name.into());
138        self
139    }
140
141    /// Check if this is an implicit many-to-many relation.
142    pub fn is_implicit_many_to_many(&self) -> bool {
143        self.relation_type == RelationType::ManyToMany && self.from_fields.is_empty()
144    }
145
146    /// Get the join table name for many-to-many relations.
147    pub fn join_table_name(&self) -> Option<String> {
148        if self.relation_type != RelationType::ManyToMany {
149            return None;
150        }
151
152        // Sort model names for consistent naming
153        let mut names = [self.from_model.as_str(), self.to_model.as_str()];
154        names.sort();
155
156        Some(format!("_{}_to_{}", names[0], names[1]))
157    }
158}
159
160/// Index definition for a model.
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162pub struct Index {
163    /// Index name (auto-generated if not specified).
164    pub name: Option<SmolStr>,
165    /// Fields included in the index.
166    pub fields: Vec<IndexField>,
167    /// Whether this is a unique index.
168    pub is_unique: bool,
169    /// Index type (btree, hash, etc.).
170    pub index_type: Option<IndexType>,
171    /// Vector distance operation (for HNSW/IVFFlat indexes).
172    pub vector_ops: Option<VectorOps>,
173    /// HNSW m parameter (max connections per layer, default 16).
174    pub hnsw_m: Option<u32>,
175    /// HNSW ef_construction parameter (size of candidate list during build, default 64).
176    pub hnsw_ef_construction: Option<u32>,
177    /// IVFFlat lists parameter (number of inverted lists, default 100).
178    pub ivfflat_lists: Option<u32>,
179}
180
181impl Index {
182    /// Create a new index.
183    pub fn new(fields: Vec<IndexField>) -> Self {
184        Self {
185            name: None,
186            fields,
187            is_unique: false,
188            index_type: None,
189            vector_ops: None,
190            hnsw_m: None,
191            hnsw_ef_construction: None,
192            ivfflat_lists: None,
193        }
194    }
195
196    /// Create a unique index.
197    pub fn unique(fields: Vec<IndexField>) -> Self {
198        Self {
199            name: None,
200            fields,
201            is_unique: true,
202            index_type: None,
203            vector_ops: None,
204            hnsw_m: None,
205            hnsw_ef_construction: None,
206            ivfflat_lists: None,
207        }
208    }
209
210    /// Set the index name.
211    pub fn with_name(mut self, name: impl Into<SmolStr>) -> Self {
212        self.name = Some(name.into());
213        self
214    }
215
216    /// Set the index type.
217    pub fn with_type(mut self, index_type: IndexType) -> Self {
218        self.index_type = Some(index_type);
219        self
220    }
221
222    /// Set the vector distance operation.
223    pub fn with_vector_ops(mut self, ops: VectorOps) -> Self {
224        self.vector_ops = Some(ops);
225        self
226    }
227
228    /// Set HNSW m parameter.
229    pub fn with_hnsw_m(mut self, m: u32) -> Self {
230        self.hnsw_m = Some(m);
231        self
232    }
233
234    /// Set HNSW ef_construction parameter.
235    pub fn with_hnsw_ef_construction(mut self, ef: u32) -> Self {
236        self.hnsw_ef_construction = Some(ef);
237        self
238    }
239
240    /// Set IVFFlat lists parameter.
241    pub fn with_ivfflat_lists(mut self, lists: u32) -> Self {
242        self.ivfflat_lists = Some(lists);
243        self
244    }
245
246    /// Check if this is a vector index.
247    pub fn is_vector_index(&self) -> bool {
248        self.index_type
249            .as_ref()
250            .is_some_and(|t| t.is_vector_index())
251    }
252}
253
254/// A field in an index.
255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
256pub struct IndexField {
257    /// Field name.
258    pub name: SmolStr,
259    /// Sort order.
260    pub sort: SortOrder,
261}
262
263impl IndexField {
264    /// Create a new index field with ascending order.
265    pub fn asc(name: impl Into<SmolStr>) -> Self {
266        Self {
267            name: name.into(),
268            sort: SortOrder::Asc,
269        }
270    }
271
272    /// Create a new index field with descending order.
273    pub fn desc(name: impl Into<SmolStr>) -> Self {
274        Self {
275            name: name.into(),
276            sort: SortOrder::Desc,
277        }
278    }
279}
280
281/// Sort order for index fields.
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
283pub enum SortOrder {
284    /// Ascending order.
285    #[default]
286    Asc,
287    /// Descending order.
288    Desc,
289}
290
291/// Index type.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
293pub enum IndexType {
294    /// B-tree index (default).
295    BTree,
296    /// Hash index.
297    Hash,
298    /// GiST index (PostgreSQL).
299    Gist,
300    /// GIN index (PostgreSQL).
301    Gin,
302    /// Full-text search index.
303    FullText,
304    /// BRIN index (PostgreSQL - Block Range Index).
305    Brin,
306    /// HNSW index for vector similarity search (pgvector).
307    Hnsw,
308    /// IVFFlat index for vector similarity search (pgvector).
309    IvfFlat,
310}
311
312impl IndexType {
313    /// Parse from string.
314    #[allow(clippy::should_implement_trait)]
315    pub fn from_str(s: &str) -> Option<Self> {
316        match s.to_lowercase().as_str() {
317            "btree" => Some(Self::BTree),
318            "hash" => Some(Self::Hash),
319            "gist" => Some(Self::Gist),
320            "gin" => Some(Self::Gin),
321            "fulltext" => Some(Self::FullText),
322            "brin" => Some(Self::Brin),
323            "hnsw" => Some(Self::Hnsw),
324            "ivfflat" => Some(Self::IvfFlat),
325            _ => None,
326        }
327    }
328
329    /// Check if this is a vector index type.
330    pub fn is_vector_index(&self) -> bool {
331        matches!(self, Self::Hnsw | Self::IvfFlat)
332    }
333
334    /// Get the SQL name for this index type.
335    pub fn as_sql(&self) -> &'static str {
336        match self {
337            Self::BTree => "BTREE",
338            Self::Hash => "HASH",
339            Self::Gist => "GIST",
340            Self::Gin => "GIN",
341            Self::FullText => "GIN", // Full-text uses GIN in PostgreSQL
342            Self::Brin => "BRIN",
343            Self::Hnsw => "hnsw",
344            Self::IvfFlat => "ivfflat",
345        }
346    }
347}
348
349/// Vector distance operation for similarity search.
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
351pub enum VectorOps {
352    /// Cosine distance (1 - cosine_similarity).
353    #[default]
354    Cosine,
355    /// L2 (Euclidean) distance.
356    L2,
357    /// Inner product (negative dot product for max inner product search).
358    InnerProduct,
359}
360
361impl VectorOps {
362    /// Parse from string.
363    #[allow(clippy::should_implement_trait)]
364    pub fn from_str(s: &str) -> Option<Self> {
365        match s.to_lowercase().as_str() {
366            "cosine" | "vector_cosine_ops" => Some(Self::Cosine),
367            "l2" | "vector_l2_ops" | "euclidean" => Some(Self::L2),
368            "ip" | "inner_product" | "vector_ip_ops" | "innerproduct" => Some(Self::InnerProduct),
369            _ => None,
370        }
371    }
372
373    /// Get the PostgreSQL operator class name for pgvector.
374    pub fn as_ops_class(&self) -> &'static str {
375        match self {
376            Self::Cosine => "vector_cosine_ops",
377            Self::L2 => "vector_l2_ops",
378            Self::InnerProduct => "vector_ip_ops",
379        }
380    }
381
382    /// Get the PostgreSQL distance operator.
383    pub fn as_operator(&self) -> &'static str {
384        match self {
385            Self::Cosine => "<=>",
386            Self::L2 => "<->",
387            Self::InnerProduct => "<#>",
388        }
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    // ==================== RelationType Tests ====================
397
398    #[test]
399    fn test_relation_type_one_to_one() {
400        let rt = RelationType::OneToOne;
401        assert!(rt.is_to_one());
402        assert!(!rt.is_to_many());
403        assert!(!rt.is_from_many());
404    }
405
406    #[test]
407    fn test_relation_type_one_to_many() {
408        let rt = RelationType::OneToMany;
409        assert!(!rt.is_to_one());
410        assert!(rt.is_to_many());
411        assert!(!rt.is_from_many());
412    }
413
414    #[test]
415    fn test_relation_type_many_to_one() {
416        let rt = RelationType::ManyToOne;
417        assert!(rt.is_to_one());
418        assert!(!rt.is_to_many());
419        assert!(rt.is_from_many());
420    }
421
422    #[test]
423    fn test_relation_type_many_to_many() {
424        let rt = RelationType::ManyToMany;
425        assert!(!rt.is_to_one());
426        assert!(rt.is_to_many());
427        assert!(rt.is_from_many());
428    }
429
430    #[test]
431    fn test_relation_type_display() {
432        assert_eq!(format!("{}", RelationType::OneToOne), "1:1");
433        assert_eq!(format!("{}", RelationType::OneToMany), "1:n");
434        assert_eq!(format!("{}", RelationType::ManyToOne), "n:1");
435        assert_eq!(format!("{}", RelationType::ManyToMany), "m:n");
436    }
437
438    #[test]
439    fn test_relation_type_equality() {
440        assert_eq!(RelationType::OneToOne, RelationType::OneToOne);
441        assert_ne!(RelationType::OneToOne, RelationType::OneToMany);
442    }
443
444    // ==================== Relation Tests ====================
445
446    #[test]
447    fn test_relation_new() {
448        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne);
449
450        assert!(rel.name.is_none());
451        assert_eq!(rel.from_model.as_str(), "Post");
452        assert_eq!(rel.from_field.as_str(), "author");
453        assert_eq!(rel.to_model.as_str(), "User");
454        assert!(rel.to_field.is_none());
455        assert_eq!(rel.relation_type, RelationType::ManyToOne);
456        assert!(rel.on_delete.is_none());
457        assert!(rel.on_update.is_none());
458    }
459
460    #[test]
461    fn test_relation_with_name() {
462        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
463            .with_name("PostAuthor");
464
465        assert_eq!(rel.name, Some("PostAuthor".into()));
466    }
467
468    #[test]
469    fn test_relation_with_from_fields() {
470        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
471            .with_from_fields(vec!["author_id".into()]);
472
473        assert_eq!(rel.from_fields, vec!["author_id".to_string()]);
474    }
475
476    #[test]
477    fn test_relation_with_to_fields() {
478        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
479            .with_to_fields(vec!["id".into()]);
480
481        assert_eq!(rel.to_fields, vec!["id".to_string()]);
482    }
483
484    #[test]
485    fn test_relation_with_to_field() {
486        let rel =
487            Relation::new("Post", "author", "User", RelationType::ManyToOne).with_to_field("posts");
488
489        assert_eq!(rel.to_field, Some("posts".into()));
490    }
491
492    #[test]
493    fn test_relation_with_on_delete() {
494        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
495            .with_on_delete(ReferentialAction::Cascade);
496
497        assert_eq!(rel.on_delete, Some(ReferentialAction::Cascade));
498    }
499
500    #[test]
501    fn test_relation_with_on_update() {
502        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
503            .with_on_update(ReferentialAction::Restrict);
504
505        assert_eq!(rel.on_update, Some(ReferentialAction::Restrict));
506    }
507
508    #[test]
509    fn test_relation_is_implicit_many_to_many_true() {
510        let rel = Relation::new("Post", "tags", "Tag", RelationType::ManyToMany);
511        assert!(rel.is_implicit_many_to_many());
512    }
513
514    #[test]
515    fn test_relation_is_implicit_many_to_many_false_explicit() {
516        let rel = Relation::new("Post", "tags", "Tag", RelationType::ManyToMany)
517            .with_from_fields(vec!["post_id".into()]);
518        assert!(!rel.is_implicit_many_to_many());
519    }
520
521    #[test]
522    fn test_relation_is_implicit_many_to_many_false_not_mtm() {
523        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne);
524        assert!(!rel.is_implicit_many_to_many());
525    }
526
527    #[test]
528    fn test_relation_join_table_name_mtm() {
529        let rel = Relation::new("Post", "tags", "Tag", RelationType::ManyToMany);
530        assert_eq!(rel.join_table_name(), Some("_Post_to_Tag".to_string()));
531    }
532
533    #[test]
534    fn test_relation_join_table_name_mtm_sorted() {
535        // Should sort alphabetically
536        let rel = Relation::new("Tag", "posts", "Post", RelationType::ManyToMany);
537        assert_eq!(rel.join_table_name(), Some("_Post_to_Tag".to_string()));
538    }
539
540    #[test]
541    fn test_relation_join_table_name_not_mtm() {
542        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne);
543        assert!(rel.join_table_name().is_none());
544    }
545
546    #[test]
547    fn test_relation_builder_chain() {
548        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
549            .with_name("PostAuthor")
550            .with_from_fields(vec!["author_id".into()])
551            .with_to_fields(vec!["id".into()])
552            .with_to_field("posts")
553            .with_on_delete(ReferentialAction::Cascade)
554            .with_on_update(ReferentialAction::Restrict);
555
556        assert_eq!(rel.name, Some("PostAuthor".into()));
557        assert_eq!(rel.from_fields.len(), 1);
558        assert_eq!(rel.to_fields.len(), 1);
559        assert!(rel.to_field.is_some());
560        assert!(rel.on_delete.is_some());
561        assert!(rel.on_update.is_some());
562    }
563
564    #[test]
565    fn test_relation_equality() {
566        let rel1 = Relation::new("Post", "author", "User", RelationType::ManyToOne);
567        let rel2 = Relation::new("Post", "author", "User", RelationType::ManyToOne);
568
569        assert_eq!(rel1, rel2);
570    }
571
572    // ==================== Index Tests ====================
573
574    #[test]
575    fn test_index_new() {
576        let idx = Index::new(vec![IndexField::asc("email")]);
577
578        assert!(idx.name.is_none());
579        assert_eq!(idx.fields.len(), 1);
580        assert!(!idx.is_unique);
581        assert!(idx.index_type.is_none());
582    }
583
584    #[test]
585    fn test_index_unique() {
586        let idx = Index::unique(vec![IndexField::asc("email")]);
587
588        assert!(idx.is_unique);
589    }
590
591    #[test]
592    fn test_index_with_name() {
593        let idx = Index::new(vec![IndexField::asc("email")]).with_name("idx_user_email");
594
595        assert_eq!(idx.name, Some("idx_user_email".into()));
596    }
597
598    #[test]
599    fn test_index_with_type() {
600        let idx = Index::new(vec![IndexField::asc("data")]).with_type(IndexType::Gin);
601
602        assert_eq!(idx.index_type, Some(IndexType::Gin));
603    }
604
605    #[test]
606    fn test_index_multiple_fields() {
607        let idx = Index::unique(vec![
608            IndexField::asc("first_name"),
609            IndexField::asc("last_name"),
610        ]);
611
612        assert_eq!(idx.fields.len(), 2);
613    }
614
615    // ==================== IndexField Tests ====================
616
617    #[test]
618    fn test_index_field_asc() {
619        let field = IndexField::asc("email");
620
621        assert_eq!(field.name.as_str(), "email");
622        assert_eq!(field.sort, SortOrder::Asc);
623    }
624
625    #[test]
626    fn test_index_field_desc() {
627        let field = IndexField::desc("created_at");
628
629        assert_eq!(field.name.as_str(), "created_at");
630        assert_eq!(field.sort, SortOrder::Desc);
631    }
632
633    #[test]
634    fn test_index_field_equality() {
635        let f1 = IndexField::asc("email");
636        let f2 = IndexField::asc("email");
637        let f3 = IndexField::desc("email");
638
639        assert_eq!(f1, f2);
640        assert_ne!(f1, f3);
641    }
642
643    // ==================== SortOrder Tests ====================
644
645    #[test]
646    fn test_sort_order_default() {
647        let order = SortOrder::default();
648        assert_eq!(order, SortOrder::Asc);
649    }
650
651    #[test]
652    fn test_sort_order_equality() {
653        assert_eq!(SortOrder::Asc, SortOrder::Asc);
654        assert_eq!(SortOrder::Desc, SortOrder::Desc);
655        assert_ne!(SortOrder::Asc, SortOrder::Desc);
656    }
657
658    // ==================== IndexType Tests ====================
659
660    #[test]
661    fn test_index_type_from_str_btree() {
662        assert_eq!(IndexType::from_str("btree"), Some(IndexType::BTree));
663        assert_eq!(IndexType::from_str("BTree"), Some(IndexType::BTree));
664        assert_eq!(IndexType::from_str("BTREE"), Some(IndexType::BTree));
665    }
666
667    #[test]
668    fn test_index_type_from_str_hash() {
669        assert_eq!(IndexType::from_str("hash"), Some(IndexType::Hash));
670        assert_eq!(IndexType::from_str("Hash"), Some(IndexType::Hash));
671    }
672
673    #[test]
674    fn test_index_type_from_str_gist() {
675        assert_eq!(IndexType::from_str("gist"), Some(IndexType::Gist));
676        assert_eq!(IndexType::from_str("GiST"), Some(IndexType::Gist));
677    }
678
679    #[test]
680    fn test_index_type_from_str_gin() {
681        assert_eq!(IndexType::from_str("gin"), Some(IndexType::Gin));
682        assert_eq!(IndexType::from_str("GIN"), Some(IndexType::Gin));
683    }
684
685    #[test]
686    fn test_index_type_from_str_fulltext() {
687        assert_eq!(IndexType::from_str("fulltext"), Some(IndexType::FullText));
688        assert_eq!(IndexType::from_str("FullText"), Some(IndexType::FullText));
689    }
690
691    #[test]
692    fn test_index_type_from_str_unknown() {
693        assert_eq!(IndexType::from_str("unknown"), None);
694        assert_eq!(IndexType::from_str(""), None);
695    }
696
697    #[test]
698    fn test_index_type_equality() {
699        assert_eq!(IndexType::BTree, IndexType::BTree);
700        assert_ne!(IndexType::BTree, IndexType::Hash);
701    }
702
703    #[test]
704    fn test_index_type_from_str_brin() {
705        assert_eq!(IndexType::from_str("brin"), Some(IndexType::Brin));
706        assert_eq!(IndexType::from_str("BRIN"), Some(IndexType::Brin));
707    }
708
709    #[test]
710    fn test_index_type_from_str_hnsw() {
711        assert_eq!(IndexType::from_str("hnsw"), Some(IndexType::Hnsw));
712        assert_eq!(IndexType::from_str("HNSW"), Some(IndexType::Hnsw));
713    }
714
715    #[test]
716    fn test_index_type_from_str_ivfflat() {
717        assert_eq!(IndexType::from_str("ivfflat"), Some(IndexType::IvfFlat));
718        assert_eq!(IndexType::from_str("IVFFLAT"), Some(IndexType::IvfFlat));
719    }
720
721    #[test]
722    fn test_index_type_is_vector_index() {
723        assert!(IndexType::Hnsw.is_vector_index());
724        assert!(IndexType::IvfFlat.is_vector_index());
725        assert!(!IndexType::BTree.is_vector_index());
726        assert!(!IndexType::Gin.is_vector_index());
727    }
728
729    #[test]
730    fn test_index_type_as_sql() {
731        assert_eq!(IndexType::BTree.as_sql(), "BTREE");
732        assert_eq!(IndexType::Hash.as_sql(), "HASH");
733        assert_eq!(IndexType::Hnsw.as_sql(), "hnsw");
734        assert_eq!(IndexType::IvfFlat.as_sql(), "ivfflat");
735    }
736
737    // ==================== VectorOps Tests ====================
738
739    #[test]
740    fn test_vector_ops_from_str_cosine() {
741        assert_eq!(VectorOps::from_str("cosine"), Some(VectorOps::Cosine));
742        assert_eq!(
743            VectorOps::from_str("vector_cosine_ops"),
744            Some(VectorOps::Cosine)
745        );
746    }
747
748    #[test]
749    fn test_vector_ops_from_str_l2() {
750        assert_eq!(VectorOps::from_str("l2"), Some(VectorOps::L2));
751        assert_eq!(VectorOps::from_str("euclidean"), Some(VectorOps::L2));
752        assert_eq!(VectorOps::from_str("vector_l2_ops"), Some(VectorOps::L2));
753    }
754
755    #[test]
756    fn test_vector_ops_from_str_inner_product() {
757        assert_eq!(VectorOps::from_str("ip"), Some(VectorOps::InnerProduct));
758        assert_eq!(
759            VectorOps::from_str("inner_product"),
760            Some(VectorOps::InnerProduct)
761        );
762        assert_eq!(
763            VectorOps::from_str("vector_ip_ops"),
764            Some(VectorOps::InnerProduct)
765        );
766    }
767
768    #[test]
769    fn test_vector_ops_as_ops_class() {
770        assert_eq!(VectorOps::Cosine.as_ops_class(), "vector_cosine_ops");
771        assert_eq!(VectorOps::L2.as_ops_class(), "vector_l2_ops");
772        assert_eq!(VectorOps::InnerProduct.as_ops_class(), "vector_ip_ops");
773    }
774
775    #[test]
776    fn test_vector_ops_as_operator() {
777        assert_eq!(VectorOps::Cosine.as_operator(), "<=>");
778        assert_eq!(VectorOps::L2.as_operator(), "<->");
779        assert_eq!(VectorOps::InnerProduct.as_operator(), "<#>");
780    }
781
782    #[test]
783    fn test_vector_ops_default() {
784        let ops = VectorOps::default();
785        assert_eq!(ops, VectorOps::Cosine);
786    }
787
788    // ==================== Index with Vector Ops Tests ====================
789
790    #[test]
791    fn test_index_with_vector_ops() {
792        let idx = Index::new(vec![IndexField::asc("embedding")])
793            .with_type(IndexType::Hnsw)
794            .with_vector_ops(VectorOps::Cosine)
795            .with_hnsw_m(16)
796            .with_hnsw_ef_construction(64);
797
798        assert_eq!(idx.index_type, Some(IndexType::Hnsw));
799        assert_eq!(idx.vector_ops, Some(VectorOps::Cosine));
800        assert_eq!(idx.hnsw_m, Some(16));
801        assert_eq!(idx.hnsw_ef_construction, Some(64));
802        assert!(idx.is_vector_index());
803    }
804
805    #[test]
806    fn test_index_with_ivfflat() {
807        let idx = Index::new(vec![IndexField::asc("embedding")])
808            .with_type(IndexType::IvfFlat)
809            .with_vector_ops(VectorOps::L2)
810            .with_ivfflat_lists(100);
811
812        assert_eq!(idx.index_type, Some(IndexType::IvfFlat));
813        assert_eq!(idx.vector_ops, Some(VectorOps::L2));
814        assert_eq!(idx.ivfflat_lists, Some(100));
815        assert!(idx.is_vector_index());
816    }
817}