yeti_types/schema/field.rs
1//! Field definition types: index config, vector/HNSW parameters, and relationships.
2
3// ============================================================================
4// Vector / HNSW configuration types
5// ============================================================================
6
7/// Distance metric for vector similarity search.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum VectorDistance {
10 /// Cosine similarity distance (1 - cosine similarity)
11 #[default]
12 Cosine,
13 /// Euclidean distance (L2 norm)
14 Euclidean,
15 /// Dot-product (inner-product) distance.
16 ///
17 /// Ranks by the inner product of two vectors: a larger inner product
18 /// means greater similarity. The HNSW index treats smaller scores as
19 /// closer, so the distance function returns the negated inner product
20 /// (see `dot_product_distance`). For L2-normalized embeddings this
21 /// ranks identically to [`VectorDistance::Cosine`] while skipping the
22 /// per-vector normalization step.
23 DotProduct,
24}
25
26/// HNSW index configuration parameters.
27#[derive(Debug, Clone, PartialEq)]
28pub struct HnswConfig {
29 /// Maximum number of connections per layer (default: 16)
30 pub m: usize,
31 /// Size of dynamic candidate list during construction (default: 100)
32 pub ef_construction: usize,
33 /// Size of dynamic candidate list during search (default: 50)
34 pub ef_search: usize,
35 /// Normalization factor for level generation: 1 / ln(M)
36 pub ml: f32,
37 /// How aggressively to avoid redundant connections (0.0 - 1.0, default: 0.5)
38 pub optimize_routing: f32,
39 /// Distance metric to use
40 pub distance: VectorDistance,
41}
42
43impl Default for HnswConfig {
44 fn default() -> Self {
45 let m = 16;
46 Self {
47 m,
48 ef_construction: 100,
49 ef_search: 50,
50 #[expect(
51 clippy::cast_precision_loss,
52 reason = "HNSW level normalizer: M is bounded by config (typically 8-64); fits in f32"
53 )]
54 ml: 1.0 / (m as f32).ln(),
55 optimize_routing: 0.5,
56 distance: VectorDistance::default(),
57 }
58 }
59}
60
61// ============================================================================
62// IndexConfig
63// ============================================================================
64
65/// Index configuration for a field.
66#[derive(Debug, Clone, PartialEq)]
67pub enum IndexConfig {
68 /// No index
69 None,
70 /// Standard secondary index (hash + range)
71 Standard,
72 /// Full-text search index (inverted index with tokenization)
73 FullText,
74 /// Vector index using HNSW algorithm
75 Vector {
76 /// HNSW index configuration
77 hnsw_config: HnswConfig,
78 /// Source field to auto-embed (None = manual vectors)
79 source: Option<String>,
80 /// Embedding model identifier (None = use default)
81 model: Option<String>,
82 },
83}
84
85impl IndexConfig {
86 /// Check if any index is configured.
87 #[must_use]
88 pub const fn is_indexed(&self) -> bool {
89 !matches!(self, Self::None)
90 }
91
92 /// Check if this is a vector index.
93 #[must_use]
94 pub const fn is_vector(&self) -> bool {
95 matches!(self, Self::Vector { .. })
96 }
97
98 /// Check if this is a full-text search index.
99 #[must_use]
100 pub const fn is_fulltext(&self) -> bool {
101 matches!(self, Self::FullText)
102 }
103}
104
105/// Composite index definition (multi-field index).
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct CompositeIndexDef {
108 /// Auto-generated name from field names (e.g., "`category_price`")
109 pub name: String,
110 /// Ordered list of field names in the composite key
111 pub fields: Vec<String>,
112}
113
114/// Field definition within a table.
115#[derive(Debug, Clone)]
116#[expect(
117 clippy::struct_excessive_bools,
118 reason = "schema-parser output; each bool maps 1:1 to a documented GraphQL directive (@primary, @createdTime, @updatedTime, @expiresAt). Bitflags would force a parallel directive enum and obscure the parser's pattern-match shape."
119)]
120pub struct FieldDefinition {
121 /// Field name (attribute name)
122 pub name: String,
123 /// GraphQL type (ID, String, Int, Boolean, etc.)
124 pub field_type: String,
125 /// Whether this field is the primary key
126 pub is_primary: bool,
127 /// Index configuration for this field
128 pub index_config: IndexConfig,
129 /// Optional relationship configuration for foreign keys
130 pub relationship: Option<RelationshipDefinition>,
131 /// Auto-assign creation timestamp (@createdTime)
132 pub assign_created_time: bool,
133 /// Auto-assign update timestamp (@updatedTime)
134 pub assign_updated_time: bool,
135 /// TTL expiration field (@expiresAt)
136 pub expires_at: bool,
137 /// Computed field expression from `@computed(from: "expr")`
138 pub computed_from: Option<String>,
139 /// Default value from `@default(value: ...)` — type-validated at parse time
140 pub default_value: Option<serde_json::Value>,
141}
142
143impl Default for FieldDefinition {
144 fn default() -> Self {
145 Self {
146 name: String::new(),
147 field_type: "String".to_owned(),
148 is_primary: false,
149 index_config: IndexConfig::None,
150 relationship: None,
151 assign_created_time: false,
152 assign_updated_time: false,
153 expires_at: false,
154 computed_from: None,
155 default_value: None,
156 }
157 }
158}
159
160/// Relationship configuration (Harper-compatible).
161#[derive(Debug, Clone)]
162pub struct RelationshipDefinition {
163 /// Foreign key attribute in this table (many-to-one)
164 pub from_attribute: Option<String>,
165 /// Foreign key attribute in target table (one-to-many)
166 pub to_attribute: Option<String>,
167}