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