Skip to main content

hermes_core/dsl/sdl/
mod.rs

1//! Schema Definition Language (SDL) for Hermes
2//!
3//! A simple, readable format for defining index schemas using pest parser.
4//!
5//! # Example SDL
6//!
7//! ```text
8//! # Article index schema
9//! index articles {
10//!     # Primary text field for full-text search
11//!     field title: text [indexed, stored]
12//!
13//!     # Body content - indexed but not stored (save space)
14//!     field body: text [indexed]
15//!
16//!     # Author name
17//!     field author: text [indexed, stored]
18//!
19//!     # Publication timestamp
20//!     field published_at: i64 [indexed, stored]
21//!
22//!     # View count
23//!     field views: u64 [indexed, stored]
24//!
25//!     # Rating score
26//!     field rating: f64 [indexed, stored]
27//!
28//!     # Raw content hash (not indexed, just stored)
29//!     field content_hash: bytes [stored]
30//!
31//!     # Dense vector with IVF-RaBitQ index
32//!     field embedding: dense_vector<768> [indexed<rabitq, centroids: "centroids.bin", nprobe: 32>]
33//!
34//! }
35//! ```
36//!
37//! # Dense Vector Index Configuration
38//!
39//! Index-related parameters for dense vectors are specified in `indexed<...>`:
40//! - `rabitq` or `scann` - index type
41//! - `centroids: "path"` - path to pre-trained centroids file
42//! - `codebook: "path"` - path to PQ codebook (ScaNN only)
43//! - `nprobe: N` - number of clusters to probe (default: 32)
44
45use pest::Parser;
46use pest_derive::Parser;
47
48use super::query_field_router::{QueryRouterRule, RoutingMode};
49use super::schema::{DenseVectorQuantization, FieldType, Schema, SchemaBuilder};
50use crate::Result;
51use crate::error::Error;
52
53#[derive(Parser)]
54#[grammar = "dsl/sdl/sdl.pest"]
55pub struct SdlParser;
56
57use super::schema::{BinaryDenseVectorConfig, DenseVectorConfig};
58use crate::structures::{
59    IndexSize, QueryWeighting, SparseFormat, SparseQueryConfig, SparseVectorConfig,
60    WeightQuantization,
61};
62
63/// Parsed field definition
64#[derive(Debug, Clone)]
65pub struct FieldDef {
66    pub name: String,
67    pub field_type: FieldType,
68    pub indexed: bool,
69    pub stored: bool,
70    /// Tokenizer name for text fields (e.g., "simple", "en_stem", "german")
71    pub tokenizer: Option<String>,
72    /// Whether this field can have multiple values (serialized as array in JSON)
73    pub multi: bool,
74    /// Position tracking mode for phrase queries and multi-field element tracking
75    pub positions: Option<super::schema::PositionMode>,
76    /// Configuration for sparse vector fields
77    pub sparse_vector_config: Option<SparseVectorConfig>,
78    /// Configuration for dense vector fields
79    pub dense_vector_config: Option<DenseVectorConfig>,
80    /// Configuration for binary dense vector fields
81    pub binary_dense_vector_config: Option<BinaryDenseVectorConfig>,
82    /// Whether this field has columnar fast-field storage
83    pub fast: bool,
84    /// Whether this field is a primary key (unique constraint)
85    pub primary: bool,
86    /// Whether build-time document reordering (BP) is enabled for BMP fields
87    pub reorder: bool,
88}
89
90/// Parsed index definition
91#[derive(Debug, Clone)]
92pub struct IndexDef {
93    pub name: String,
94    pub fields: Vec<FieldDef>,
95    pub default_fields: Vec<String>,
96    /// Query router rules for routing queries to specific fields
97    pub query_routers: Vec<QueryRouterRule>,
98}
99
100impl IndexDef {
101    /// Convert to a Schema
102    pub fn to_schema(&self) -> Schema {
103        let mut builder = SchemaBuilder::default();
104
105        for field in &self.fields {
106            let f = match field.field_type {
107                FieldType::Text => {
108                    let tokenizer = field.tokenizer.as_deref().unwrap_or("simple");
109                    builder.add_text_field_with_tokenizer(
110                        &field.name,
111                        field.indexed,
112                        field.stored,
113                        tokenizer,
114                    )
115                }
116                FieldType::U64 => builder.add_u64_field(&field.name, field.indexed, field.stored),
117                FieldType::I64 => builder.add_i64_field(&field.name, field.indexed, field.stored),
118                FieldType::F64 => builder.add_f64_field(&field.name, field.indexed, field.stored),
119                FieldType::Bytes => builder.add_bytes_field(&field.name, field.stored),
120                FieldType::Json => builder.add_json_field(&field.name, field.stored),
121                FieldType::SparseVector => {
122                    if let Some(config) = &field.sparse_vector_config {
123                        builder.add_sparse_vector_field_with_config(
124                            &field.name,
125                            field.indexed,
126                            field.stored,
127                            config.clone(),
128                        )
129                    } else {
130                        builder.add_sparse_vector_field(&field.name, field.indexed, field.stored)
131                    }
132                }
133                FieldType::DenseVector => {
134                    // Dense vector dimension must be specified via config
135                    let config = field
136                        .dense_vector_config
137                        .as_ref()
138                        .expect("DenseVector field requires dimension to be specified");
139                    builder.add_dense_vector_field_with_config(
140                        &field.name,
141                        field.indexed,
142                        field.stored,
143                        config.clone(),
144                    )
145                }
146                FieldType::BinaryDenseVector => {
147                    let config = field
148                        .binary_dense_vector_config
149                        .as_ref()
150                        .expect("BinaryDenseVector field requires dimension to be specified");
151                    builder.add_binary_dense_vector_field_with_config(
152                        &field.name,
153                        field.indexed,
154                        field.stored,
155                        config.clone(),
156                    )
157                }
158            };
159            if field.multi {
160                builder.set_multi(f, true);
161            }
162            if field.fast {
163                builder.set_fast(f, true);
164            }
165            if field.primary {
166                builder.set_primary_key(f);
167            }
168            if field.reorder {
169                builder.set_reorder(f, true);
170            }
171            // Set positions: explicit > auto (ordinal for multi vectors)
172            let positions = field.positions.or({
173                // Auto-set ordinal positions for multi-valued vector fields
174                if field.multi
175                    && matches!(
176                        field.field_type,
177                        FieldType::SparseVector
178                            | FieldType::DenseVector
179                            | FieldType::BinaryDenseVector
180                    )
181                {
182                    Some(super::schema::PositionMode::Ordinal)
183                } else {
184                    None
185                }
186            });
187            if let Some(mode) = positions {
188                builder.set_positions(f, mode);
189            }
190        }
191
192        // Set default fields if specified
193        if !self.default_fields.is_empty() {
194            builder.set_default_fields(self.default_fields.clone());
195        }
196
197        // Set query routers if specified
198        if !self.query_routers.is_empty() {
199            builder.set_query_routers(self.query_routers.clone());
200        }
201
202        builder.build()
203    }
204
205    /// Create a QueryFieldRouter from the query router rules
206    ///
207    /// Returns None if there are no query router rules defined.
208    /// Returns Err if any regex pattern is invalid.
209    pub fn to_query_router(&self) -> Result<Option<super::query_field_router::QueryFieldRouter>> {
210        if self.query_routers.is_empty() {
211            return Ok(None);
212        }
213
214        super::query_field_router::QueryFieldRouter::from_rules(&self.query_routers)
215            .map(Some)
216            .map_err(Error::Schema)
217    }
218}
219
220/// Parse field type from string
221fn parse_field_type(type_str: &str) -> Result<FieldType> {
222    match type_str {
223        "text" | "string" | "str" => Ok(FieldType::Text),
224        "u64" | "uint" | "unsigned" => Ok(FieldType::U64),
225        "i64" | "int" | "integer" => Ok(FieldType::I64),
226        "f64" | "float" | "double" => Ok(FieldType::F64),
227        "bytes" | "binary" | "blob" => Ok(FieldType::Bytes),
228        "json" => Ok(FieldType::Json),
229        "sparse_vector" => Ok(FieldType::SparseVector),
230        "dense_vector" | "vector" => Ok(FieldType::DenseVector),
231        "binary_dense_vector" | "binary_vector" => Ok(FieldType::BinaryDenseVector),
232        _ => Err(Error::Schema(format!("Unknown field type: {}", type_str))),
233    }
234}
235
236/// Index configuration parsed from indexed<...> attribute
237#[derive(Debug, Clone, Default)]
238struct IndexConfig {
239    index_type: Option<super::schema::VectorIndexType>,
240    num_clusters: Option<usize>,
241    nprobe: Option<usize>,
242    build_threshold: Option<usize>,
243    soar: Option<crate::structures::SoarConfig>,
244    // Sparse vector index params
245    sparse_format: Option<SparseFormat>,
246    quantization: Option<WeightQuantization>,
247    weight_threshold: Option<f32>,
248    block_size: Option<usize>,
249    pruning: Option<f32>,
250    min_terms: Option<usize>,
251    // Sparse vector query-time config
252    query_tokenizer: Option<String>,
253    query_weighting: Option<QueryWeighting>,
254    query_weight_threshold: Option<f32>,
255    query_max_dims: Option<usize>,
256    query_pruning: Option<f32>,
257    query_min_query_dims: Option<usize>,
258    // BMP fixed dims (vocabulary size) and max weight scale
259    dims: Option<u32>,
260    max_weight: Option<f32>,
261    // Position tracking mode for phrase queries
262    positions: Option<super::schema::PositionMode>,
263}
264
265/// Parsed attributes from SDL field definition
266struct ParsedAttributes {
267    indexed: bool,
268    stored: bool,
269    multi: bool,
270    fast: bool,
271    primary: bool,
272    reorder: bool,
273    index_config: Option<IndexConfig>,
274}
275
276/// Parse attributes from pest pair
277fn parse_attributes(pair: pest::iterators::Pair<Rule>) -> ParsedAttributes {
278    let mut attrs = ParsedAttributes {
279        indexed: false,
280        stored: false,
281        multi: false,
282        fast: false,
283        primary: false,
284        reorder: false,
285        index_config: None,
286    };
287
288    for attr in pair.into_inner() {
289        if attr.as_rule() == Rule::attribute {
290            let mut found_config = false;
291            for inner in attr.clone().into_inner() {
292                match inner.as_rule() {
293                    Rule::indexed_with_config => {
294                        attrs.indexed = true;
295                        attrs.index_config = Some(parse_index_config(inner));
296                        found_config = true;
297                        break;
298                    }
299                    Rule::stored_with_config => {
300                        attrs.stored = true;
301                        attrs.multi = true; // stored<multi>
302                        found_config = true;
303                        break;
304                    }
305                    _ => {}
306                }
307            }
308            if !found_config {
309                match attr.as_str() {
310                    "indexed" => attrs.indexed = true,
311                    "stored" => attrs.stored = true,
312                    "fast" => attrs.fast = true,
313                    "primary" => attrs.primary = true,
314                    "reorder" => attrs.reorder = true,
315                    _ => {}
316                }
317            }
318        }
319    }
320
321    attrs
322}
323
324/// Parse index configuration from indexed<...> attribute
325fn parse_index_config(pair: pest::iterators::Pair<Rule>) -> IndexConfig {
326    let mut config = IndexConfig::default();
327
328    // indexed_with_config = { "indexed" ~ "<" ~ index_config_params ~ ">" }
329    // index_config_params = { index_config_param ~ ("," ~ index_config_param)* }
330    // index_config_param = { index_type_kwarg | centroids_kwarg | codebook_kwarg | nprobe_kwarg | index_type_spec }
331
332    for inner in pair.into_inner() {
333        if inner.as_rule() == Rule::index_config_params {
334            for param in inner.into_inner() {
335                if param.as_rule() == Rule::index_config_param {
336                    for p in param.into_inner() {
337                        parse_single_index_config_param(&mut config, p);
338                    }
339                }
340            }
341        }
342    }
343
344    config
345}
346
347/// Parse a single index config parameter
348fn parse_single_index_config_param(config: &mut IndexConfig, p: pest::iterators::Pair<Rule>) {
349    use super::schema::VectorIndexType;
350
351    match p.as_rule() {
352        Rule::index_type_spec => {
353            config.index_type = Some(match p.as_str() {
354                "flat" => VectorIndexType::Flat,
355                "rabitq" => VectorIndexType::RaBitQ,
356                "ivf_rabitq" => VectorIndexType::IvfRaBitQ,
357                "scann" => VectorIndexType::ScaNN,
358                _ => VectorIndexType::RaBitQ,
359            });
360        }
361        Rule::index_type_kwarg => {
362            // index_type_kwarg = { "index" ~ ":" ~ index_type_spec }
363            if let Some(t) = p.into_inner().next() {
364                config.index_type = Some(match t.as_str() {
365                    "flat" => VectorIndexType::Flat,
366                    "rabitq" => VectorIndexType::RaBitQ,
367                    "ivf_rabitq" => VectorIndexType::IvfRaBitQ,
368                    "scann" => VectorIndexType::ScaNN,
369                    _ => VectorIndexType::RaBitQ,
370                });
371            }
372        }
373        Rule::num_clusters_kwarg => {
374            // num_clusters_kwarg = { "num_clusters" ~ ":" ~ num_clusters_spec }
375            if let Some(n) = p.into_inner().next() {
376                config.num_clusters = Some(n.as_str().parse().unwrap_or_else(|_| {
377                    log::warn!(
378                        "Invalid num_clusters value '{}', using default 256",
379                        n.as_str()
380                    );
381                    256
382                }));
383            }
384        }
385        Rule::build_threshold_kwarg => {
386            // build_threshold_kwarg = { "build_threshold" ~ ":" ~ build_threshold_spec }
387            if let Some(n) = p.into_inner().next() {
388                config.build_threshold = Some(n.as_str().parse().unwrap_or_else(|_| {
389                    log::warn!(
390                        "Invalid build_threshold value '{}', using default 10000",
391                        n.as_str()
392                    );
393                    10000
394                }));
395            }
396        }
397        Rule::nprobe_kwarg => {
398            // nprobe_kwarg = { "nprobe" ~ ":" ~ nprobe_spec }
399            if let Some(n) = p.into_inner().next() {
400                config.nprobe = Some(n.as_str().parse().unwrap_or_else(|_| {
401                    log::warn!("Invalid nprobe value '{}', using default 32", n.as_str());
402                    32
403                }));
404            }
405        }
406        Rule::soar_kwarg => {
407            // soar_kwarg = { "soar" ~ ":" ~ soar_spec }
408            if let Some(s) = p.into_inner().next() {
409                use crate::structures::SoarConfig;
410                config.soar = match s.as_str() {
411                    "selective" => Some(SoarConfig::new()),
412                    "full" => Some(SoarConfig::full()),
413                    "aggressive" => Some(SoarConfig::aggressive()),
414                    _ => None, // "off"
415                };
416            }
417        }
418        Rule::quantization_kwarg => {
419            // quantization_kwarg = { "quantization" ~ ":" ~ quantization_spec }
420            if let Some(q) = p.into_inner().next() {
421                config.quantization = Some(match q.as_str() {
422                    "float32" | "f32" => WeightQuantization::Float32,
423                    "float16" | "f16" => WeightQuantization::Float16,
424                    "uint8" | "u8" => WeightQuantization::UInt8,
425                    "uint4" | "u4" => WeightQuantization::UInt4,
426                    _ => WeightQuantization::default(),
427                });
428            }
429        }
430        Rule::weight_threshold_kwarg => {
431            // weight_threshold_kwarg = { "weight_threshold" ~ ":" ~ weight_threshold_spec }
432            if let Some(t) = p.into_inner().next() {
433                config.weight_threshold = Some(t.as_str().parse().unwrap_or_else(|_| {
434                    log::warn!(
435                        "Invalid weight_threshold value '{}', using default 0.0",
436                        t.as_str()
437                    );
438                    0.0
439                }));
440            }
441        }
442        Rule::block_size_kwarg => {
443            // block_size_kwarg = { "block_size" ~ ":" ~ block_size_spec }
444            if let Some(n) = p.into_inner().next() {
445                config.block_size = Some(n.as_str().parse().unwrap_or_else(|_| {
446                    log::warn!(
447                        "Invalid block_size value '{}', using default 128",
448                        n.as_str()
449                    );
450                    128
451                }));
452            }
453        }
454        Rule::pruning_kwarg => {
455            // pruning_kwarg = { "pruning" ~ ":" ~ pruning_spec }
456            if let Some(f) = p.into_inner().next() {
457                config.pruning = Some(f.as_str().parse().unwrap_or_else(|_| {
458                    log::warn!("Invalid pruning value '{}', using default 1.0", f.as_str());
459                    1.0
460                }));
461            }
462        }
463        Rule::min_terms_kwarg => {
464            if let Some(n) = p.into_inner().next() {
465                config.min_terms = Some(n.as_str().parse().unwrap_or_else(|_| {
466                    log::warn!("Invalid min_terms value '{}', using default 4", n.as_str());
467                    4
468                }));
469            }
470        }
471        Rule::sparse_format_kwarg => {
472            // sparse_format_kwarg = { "format" ~ ":" ~ sparse_format_spec }
473            if let Some(f) = p.into_inner().next() {
474                config.sparse_format = Some(match f.as_str() {
475                    "bmp" => SparseFormat::Bmp,
476                    "maxscore" => SparseFormat::MaxScore,
477                    _ => SparseFormat::default(),
478                });
479            }
480        }
481        Rule::sparse_dims_kwarg => {
482            if let Some(n) = p.into_inner().next() {
483                config.dims = Some(n.as_str().parse().unwrap_or_else(|_| {
484                    log::warn!("Invalid dims value '{}', using default 105879", n.as_str());
485                    105879
486                }));
487            }
488        }
489        Rule::sparse_max_weight_kwarg => {
490            if let Some(f) = p.into_inner().next() {
491                config.max_weight = Some(f.as_str().parse().unwrap_or_else(|_| {
492                    log::warn!(
493                        "Invalid max_weight value '{}', using default 5.0",
494                        f.as_str()
495                    );
496                    5.0
497                }));
498            }
499        }
500        Rule::query_config_block => {
501            // query_config_block = { "query" ~ "<" ~ query_config_params ~ ">" }
502            parse_query_config_block(config, p);
503        }
504        Rule::positions_kwarg => {
505            // positions_kwarg = { "positions" | "ordinal" | "token_position" }
506            use super::schema::PositionMode;
507            config.positions = Some(match p.as_str() {
508                "ordinal" => PositionMode::Ordinal,
509                "token_position" => PositionMode::TokenPosition,
510                _ => PositionMode::Full, // "positions" or any other value defaults to Full
511            });
512        }
513        _ => {}
514    }
515}
516
517/// Parse query configuration block: query<tokenizer: "...", weighting: idf>
518fn parse_query_config_block(config: &mut IndexConfig, pair: pest::iterators::Pair<Rule>) {
519    for inner in pair.into_inner() {
520        if inner.as_rule() == Rule::query_config_params {
521            for param in inner.into_inner() {
522                if param.as_rule() == Rule::query_config_param {
523                    for p in param.into_inner() {
524                        match p.as_rule() {
525                            Rule::query_tokenizer_kwarg => {
526                                // query_tokenizer_kwarg = { "tokenizer" ~ ":" ~ tokenizer_path }
527                                if let Some(path) = p.into_inner().next()
528                                    && let Some(inner_path) = path.into_inner().next()
529                                {
530                                    config.query_tokenizer = Some(inner_path.as_str().to_string());
531                                }
532                            }
533                            Rule::query_weighting_kwarg => {
534                                // query_weighting_kwarg = { "weighting" ~ ":" ~ weighting_spec }
535                                if let Some(w) = p.into_inner().next() {
536                                    config.query_weighting = Some(match w.as_str() {
537                                        "one" => QueryWeighting::One,
538                                        "idf" => QueryWeighting::Idf,
539                                        "idf_file" => QueryWeighting::IdfFile,
540                                        _ => QueryWeighting::One,
541                                    });
542                                }
543                            }
544                            Rule::query_weight_threshold_kwarg => {
545                                if let Some(t) = p.into_inner().next() {
546                                    config.query_weight_threshold =
547                                        Some(t.as_str().parse().unwrap_or_else(|_| {
548                                            log::warn!(
549                                                "Invalid query weight_threshold '{}', using 0.0",
550                                                t.as_str()
551                                            );
552                                            0.0
553                                        }));
554                                }
555                            }
556                            Rule::query_max_dims_kwarg => {
557                                if let Some(t) = p.into_inner().next() {
558                                    config.query_max_dims =
559                                        Some(t.as_str().parse().unwrap_or_else(|_| {
560                                            log::warn!(
561                                                "Invalid query max_dims '{}', using 0",
562                                                t.as_str()
563                                            );
564                                            0
565                                        }));
566                                }
567                            }
568                            Rule::query_pruning_kwarg => {
569                                if let Some(t) = p.into_inner().next() {
570                                    config.query_pruning =
571                                        Some(t.as_str().parse().unwrap_or_else(|_| {
572                                            log::warn!(
573                                                "Invalid query pruning '{}', using 1.0",
574                                                t.as_str()
575                                            );
576                                            1.0
577                                        }));
578                                }
579                            }
580                            Rule::query_min_query_dims_kwarg => {
581                                if let Some(t) = p.into_inner().next() {
582                                    config.query_min_query_dims =
583                                        Some(t.as_str().parse().unwrap_or_else(|_| {
584                                            log::warn!(
585                                                "Invalid query min_query_dims '{}', using 4",
586                                                t.as_str()
587                                            );
588                                            4
589                                        }));
590                                }
591                            }
592                            _ => {}
593                        }
594                    }
595                }
596            }
597        }
598    }
599}
600
601/// Parse a field definition from pest pair
602fn parse_field_def(pair: pest::iterators::Pair<Rule>) -> Result<FieldDef> {
603    let mut inner = pair.into_inner();
604
605    let name = inner
606        .next()
607        .ok_or_else(|| Error::Schema("Missing field name".to_string()))?
608        .as_str()
609        .to_string();
610
611    let field_type_str = inner
612        .next()
613        .ok_or_else(|| Error::Schema("Missing field type".to_string()))?
614        .as_str();
615
616    let field_type = parse_field_type(field_type_str)?;
617
618    // Parse optional tokenizer spec, sparse_vector_config, dense_vector_config, and attributes
619    let mut tokenizer = None;
620    let mut sparse_vector_config = None;
621    let mut dense_vector_config = None;
622    let mut binary_dense_vector_config = None;
623    let mut indexed = true;
624    let mut stored = true;
625    let mut multi = false;
626    let mut fast = false;
627    let mut primary = false;
628    let mut reorder = false;
629    let mut index_config: Option<IndexConfig> = None;
630
631    for item in inner {
632        match item.as_rule() {
633            Rule::tokenizer_spec => {
634                // Extract tokenizer name from <name>
635                if let Some(tok_name) = item.into_inner().next() {
636                    tokenizer = Some(tok_name.as_str().to_string());
637                }
638            }
639            Rule::sparse_vector_config => {
640                // Parse named parameters: <index_size: u16, quantization: uint8, weight_threshold: 0.1>
641                sparse_vector_config = Some(parse_sparse_vector_config(item));
642            }
643            Rule::dense_vector_config => {
644                // Parse dense_vector_params (keyword or positional) - only dims
645                dense_vector_config = Some(parse_dense_vector_config(item));
646            }
647            Rule::binary_dense_vector_config => {
648                // Parse binary dense vector config - just dimension (number of bits)
649                let dim: usize = item
650                    .into_inner()
651                    .next()
652                    .map(|d| d.as_str().parse().unwrap_or(0))
653                    .unwrap_or(0);
654                if dim == 0 || !dim.is_multiple_of(8) {
655                    return Err(Error::Schema(format!(
656                        "BinaryDenseVector dimension must be a positive multiple of 8, got {dim}"
657                    )));
658                }
659                binary_dense_vector_config = Some(BinaryDenseVectorConfig::new(dim));
660            }
661            Rule::attributes => {
662                let attrs = parse_attributes(item);
663                indexed = attrs.indexed;
664                stored = attrs.stored;
665                multi = attrs.multi;
666                fast = attrs.fast;
667                primary = attrs.primary;
668                reorder = attrs.reorder;
669                index_config = attrs.index_config;
670            }
671            _ => {}
672        }
673    }
674
675    // PEG grammar ambiguity: both dense_vector_config and binary_dense_vector_config
676    // match `<N>`, and dense_vector_config comes first in the ordered choice. When the
677    // field_type is BinaryDenseVector, remap the matched dense_vector_config.
678    if field_type == FieldType::BinaryDenseVector
679        && binary_dense_vector_config.is_none()
680        && let Some(ref dv_config) = dense_vector_config
681    {
682        let dim = dv_config.dim;
683        if dim == 0 || !dim.is_multiple_of(8) {
684            return Err(Error::Schema(format!(
685                "BinaryDenseVector dimension must be a positive multiple of 8, got {dim}"
686            )));
687        }
688        binary_dense_vector_config = Some(BinaryDenseVectorConfig::new(dim));
689        dense_vector_config = None;
690    }
691
692    // Primary key implies fast + indexed (needed for dedup lookups)
693    if primary {
694        fast = true;
695        indexed = true;
696    }
697
698    // Merge index config into vector configs if both exist
699    let mut positions = None;
700    if let Some(idx_cfg) = index_config {
701        positions = idx_cfg.positions;
702        if let Some(ref mut dv_config) = dense_vector_config {
703            apply_index_config_to_dense_vector(dv_config, idx_cfg);
704        } else if field_type == FieldType::SparseVector {
705            // For sparse vectors, create default config if not present and apply index params
706            let sv_config = sparse_vector_config.get_or_insert(SparseVectorConfig::default());
707            apply_index_config_to_sparse_vector(sv_config, idx_cfg);
708        }
709    }
710
711    Ok(FieldDef {
712        name,
713        field_type,
714        indexed,
715        stored,
716        tokenizer,
717        multi,
718        positions,
719        sparse_vector_config,
720        dense_vector_config,
721        binary_dense_vector_config,
722        fast,
723        primary,
724        reorder,
725    })
726}
727
728/// Apply index configuration from indexed<...> to DenseVectorConfig
729fn apply_index_config_to_dense_vector(config: &mut DenseVectorConfig, idx_cfg: IndexConfig) {
730    // Apply index type if specified
731    if let Some(index_type) = idx_cfg.index_type {
732        config.index_type = index_type;
733    }
734
735    // Apply num_clusters for IVF-based indexes
736    if idx_cfg.num_clusters.is_some() {
737        config.num_clusters = idx_cfg.num_clusters;
738    }
739
740    // Apply nprobe if specified
741    if let Some(nprobe) = idx_cfg.nprobe {
742        config.nprobe = nprobe;
743    }
744
745    // Apply build_threshold if specified
746    if idx_cfg.build_threshold.is_some() {
747        config.build_threshold = idx_cfg.build_threshold;
748    }
749
750    // Apply SOAR spilling if specified (IVF-based indexes only)
751    if idx_cfg.soar.is_some() {
752        if config.uses_ivf() {
753            config.soar = idx_cfg.soar;
754        } else {
755            log::warn!(
756                "'soar' requires an IVF-based index (ivf_rabitq, scann); \
757                 ignoring for index type {:?}",
758                config.index_type
759            );
760        }
761    }
762}
763
764/// Parse sparse_vector_config - only index_size (positional)
765/// Example: <u16> or <u32>
766fn parse_sparse_vector_config(pair: pest::iterators::Pair<Rule>) -> SparseVectorConfig {
767    let mut index_size = IndexSize::default();
768
769    // Parse positional index_size_spec
770    for inner in pair.into_inner() {
771        if inner.as_rule() == Rule::index_size_spec {
772            index_size = match inner.as_str() {
773                "u16" => IndexSize::U16,
774                "u32" => IndexSize::U32,
775                _ => IndexSize::default(),
776            };
777        }
778    }
779
780    SparseVectorConfig {
781        format: SparseFormat::default(),
782        index_size,
783        weight_quantization: WeightQuantization::default(),
784        weight_threshold: 0.0,
785        block_size: 128,
786        bmp_block_size: 64,
787        max_bmp_grid_bytes: 0,
788        bmp_superblock_size: 64,
789        pruning: None,
790        query_config: None,
791        dims: None,
792        max_weight: None,
793        min_terms: 4,
794    }
795}
796
797/// Apply index configuration from indexed<...> to SparseVectorConfig
798fn apply_index_config_to_sparse_vector(config: &mut SparseVectorConfig, idx_cfg: IndexConfig) {
799    if let Some(f) = idx_cfg.sparse_format {
800        config.format = f;
801    }
802    if let Some(q) = idx_cfg.quantization {
803        config.weight_quantization = q;
804    }
805    if let Some(t) = idx_cfg.weight_threshold {
806        config.weight_threshold = t;
807    }
808    if let Some(bs) = idx_cfg.block_size {
809        let adjusted = bs.next_power_of_two();
810        if adjusted != bs {
811            log::warn!(
812                "block_size {} adjusted to next power of two: {}",
813                bs,
814                adjusted
815            );
816        }
817        config.block_size = adjusted;
818    }
819    if let Some(p) = idx_cfg.pruning {
820        let clamped = p.clamp(0.0, 1.0);
821        if (clamped - p).abs() > f32::EPSILON {
822            log::warn!(
823                "pruning {} clamped to valid range [0.0, 1.0]: {}",
824                p,
825                clamped
826            );
827        }
828        config.pruning = Some(clamped);
829    }
830    if let Some(mt) = idx_cfg.min_terms {
831        config.min_terms = mt;
832    }
833    if let Some(d) = idx_cfg.dims {
834        config.dims = Some(d);
835    }
836    if let Some(mw) = idx_cfg.max_weight {
837        config.max_weight = Some(mw);
838    }
839    // Apply query-time configuration if present
840    if idx_cfg.query_tokenizer.is_some()
841        || idx_cfg.query_weighting.is_some()
842        || idx_cfg.query_weight_threshold.is_some()
843        || idx_cfg.query_max_dims.is_some()
844        || idx_cfg.query_pruning.is_some()
845        || idx_cfg.query_min_query_dims.is_some()
846    {
847        let query_config = config
848            .query_config
849            .get_or_insert(SparseQueryConfig::default());
850        if let Some(tokenizer) = idx_cfg.query_tokenizer {
851            query_config.tokenizer = Some(tokenizer);
852        }
853        if let Some(weighting) = idx_cfg.query_weighting {
854            query_config.weighting = weighting;
855        }
856        if let Some(t) = idx_cfg.query_weight_threshold {
857            query_config.weight_threshold = t;
858        }
859        if let Some(d) = idx_cfg.query_max_dims {
860            query_config.max_query_dims = Some(d);
861        }
862        if let Some(p) = idx_cfg.query_pruning {
863            query_config.pruning = Some(p);
864        }
865        if let Some(m) = idx_cfg.query_min_query_dims {
866            query_config.min_query_dims = m;
867        }
868    }
869}
870
871/// Parse dense_vector_config - dims and optional quantization type
872/// All index-related params are in indexed<...> attribute
873fn parse_dense_vector_config(pair: pest::iterators::Pair<Rule>) -> DenseVectorConfig {
874    let mut dim: usize = 0;
875    let mut quantization = DenseVectorQuantization::F32;
876
877    // Navigate to dense_vector_params
878    for params in pair.into_inner() {
879        if params.as_rule() == Rule::dense_vector_params {
880            for inner in params.into_inner() {
881                match inner.as_rule() {
882                    Rule::dense_vector_keyword_params => {
883                        for kwarg in inner.into_inner() {
884                            match kwarg.as_rule() {
885                                Rule::dims_kwarg => {
886                                    if let Some(d) = kwarg.into_inner().next() {
887                                        dim = d.as_str().parse().unwrap_or(0);
888                                    }
889                                }
890                                Rule::quant_type_spec => {
891                                    quantization = parse_quant_type(kwarg.as_str());
892                                }
893                                _ => {}
894                            }
895                        }
896                    }
897                    Rule::dense_vector_positional_params => {
898                        for item in inner.into_inner() {
899                            match item.as_rule() {
900                                Rule::dimension_spec => {
901                                    dim = item.as_str().parse().unwrap_or(0);
902                                }
903                                Rule::quant_type_spec => {
904                                    quantization = parse_quant_type(item.as_str());
905                                }
906                                _ => {}
907                            }
908                        }
909                    }
910                    _ => {}
911                }
912            }
913        }
914    }
915
916    DenseVectorConfig::new(dim).with_quantization(quantization)
917}
918
919fn parse_quant_type(s: &str) -> DenseVectorQuantization {
920    match s.trim() {
921        "f16" => DenseVectorQuantization::F16,
922        "uint8" | "u8" => DenseVectorQuantization::UInt8,
923        _ => DenseVectorQuantization::F32,
924    }
925}
926
927/// Parse default_fields definition
928fn parse_default_fields_def(pair: pest::iterators::Pair<Rule>) -> Vec<String> {
929    pair.into_inner().map(|p| p.as_str().to_string()).collect()
930}
931
932/// Parse a query router definition
933fn parse_query_router_def(pair: pest::iterators::Pair<Rule>) -> Result<QueryRouterRule> {
934    let mut pattern = String::new();
935    let mut substitution = String::new();
936    let mut target_field = String::new();
937    let mut mode = RoutingMode::Additional;
938
939    for prop in pair.into_inner() {
940        if prop.as_rule() != Rule::query_router_prop {
941            continue;
942        }
943
944        for inner in prop.into_inner() {
945            match inner.as_rule() {
946                Rule::query_router_pattern => {
947                    if let Some(regex_str) = inner.into_inner().next() {
948                        pattern = parse_string_value(regex_str);
949                    }
950                }
951                Rule::query_router_substitution => {
952                    if let Some(quoted) = inner.into_inner().next() {
953                        substitution = parse_string_value(quoted);
954                    }
955                }
956                Rule::query_router_target => {
957                    if let Some(ident) = inner.into_inner().next() {
958                        target_field = ident.as_str().to_string();
959                    }
960                }
961                Rule::query_router_mode => {
962                    if let Some(mode_val) = inner.into_inner().next() {
963                        mode = match mode_val.as_str() {
964                            "exclusive" => RoutingMode::Exclusive,
965                            "additional" => RoutingMode::Additional,
966                            _ => RoutingMode::Additional,
967                        };
968                    }
969                }
970                _ => {}
971            }
972        }
973    }
974
975    if pattern.is_empty() {
976        return Err(Error::Schema("query_router missing 'pattern'".to_string()));
977    }
978    if substitution.is_empty() {
979        return Err(Error::Schema(
980            "query_router missing 'substitution'".to_string(),
981        ));
982    }
983    if target_field.is_empty() {
984        return Err(Error::Schema(
985            "query_router missing 'target_field'".to_string(),
986        ));
987    }
988
989    Ok(QueryRouterRule {
990        pattern,
991        substitution,
992        target_field,
993        mode,
994    })
995}
996
997/// Parse a string value from quoted_string, raw_string, or regex_string
998fn parse_string_value(pair: pest::iterators::Pair<Rule>) -> String {
999    let s = pair.as_str();
1000    match pair.as_rule() {
1001        Rule::regex_string => {
1002            // regex_string contains either raw_string or quoted_string
1003            if let Some(inner) = pair.into_inner().next() {
1004                parse_string_value(inner)
1005            } else {
1006                s.to_string()
1007            }
1008        }
1009        Rule::raw_string => {
1010            // r"..." - strip r" prefix and " suffix
1011            s[2..s.len() - 1].to_string()
1012        }
1013        Rule::quoted_string => {
1014            // "..." - strip quotes and handle escapes
1015            let inner = &s[1..s.len() - 1];
1016            // Simple escape handling
1017            inner
1018                .replace("\\n", "\n")
1019                .replace("\\t", "\t")
1020                .replace("\\\"", "\"")
1021                .replace("\\\\", "\\")
1022        }
1023        _ => s.to_string(),
1024    }
1025}
1026
1027/// Parse an index definition from pest pair
1028fn parse_index_def(pair: pest::iterators::Pair<Rule>) -> Result<IndexDef> {
1029    let mut inner = pair.into_inner();
1030
1031    let name = inner
1032        .next()
1033        .ok_or_else(|| Error::Schema("Missing index name".to_string()))?
1034        .as_str()
1035        .to_string();
1036
1037    let mut fields = Vec::new();
1038    let mut default_fields = Vec::new();
1039    let mut query_routers = Vec::new();
1040
1041    for item in inner {
1042        match item.as_rule() {
1043            Rule::field_def => {
1044                fields.push(parse_field_def(item)?);
1045            }
1046            Rule::default_fields_def => {
1047                default_fields = parse_default_fields_def(item);
1048            }
1049            Rule::query_router_def => {
1050                query_routers.push(parse_query_router_def(item)?);
1051            }
1052            _ => {}
1053        }
1054    }
1055
1056    // Validate primary key constraints
1057    let primary_fields: Vec<&FieldDef> = fields.iter().filter(|f| f.primary).collect();
1058    if primary_fields.len() > 1 {
1059        return Err(Error::Schema(format!(
1060            "Index '{}' has {} primary key fields, but at most one is allowed",
1061            name,
1062            primary_fields.len()
1063        )));
1064    }
1065    if let Some(pk) = primary_fields.first() {
1066        if pk.field_type != FieldType::Text {
1067            return Err(Error::Schema(format!(
1068                "Primary key field '{}' must be of type text, got {:?}",
1069                pk.name, pk.field_type
1070            )));
1071        }
1072        if pk.multi {
1073            return Err(Error::Schema(format!(
1074                "Primary key field '{}' cannot be multi-valued",
1075                pk.name
1076            )));
1077        }
1078    }
1079
1080    Ok(IndexDef {
1081        name,
1082        fields,
1083        default_fields,
1084        query_routers,
1085    })
1086}
1087
1088/// Parse SDL from a string
1089pub fn parse_sdl(input: &str) -> Result<Vec<IndexDef>> {
1090    let pairs = SdlParser::parse(Rule::file, input)
1091        .map_err(|e| Error::Schema(format!("Parse error: {}", e)))?;
1092
1093    let mut indexes = Vec::new();
1094
1095    for pair in pairs {
1096        if pair.as_rule() == Rule::file {
1097            for inner in pair.into_inner() {
1098                if inner.as_rule() == Rule::index_def {
1099                    indexes.push(parse_index_def(inner)?);
1100                }
1101            }
1102        }
1103    }
1104
1105    Ok(indexes)
1106}
1107
1108/// Parse SDL and return a single index definition
1109pub fn parse_single_index(input: &str) -> Result<IndexDef> {
1110    let indexes = parse_sdl(input)?;
1111
1112    if indexes.is_empty() {
1113        return Err(Error::Schema("No index definition found".to_string()));
1114    }
1115
1116    if indexes.len() > 1 {
1117        return Err(Error::Schema(
1118            "Multiple index definitions found, expected one".to_string(),
1119        ));
1120    }
1121
1122    Ok(indexes.into_iter().next().unwrap())
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use super::*;
1128
1129    #[test]
1130    fn test_parse_simple_schema() {
1131        let sdl = r#"
1132            index articles {
1133                field title: text [indexed, stored]
1134                field body: text [indexed]
1135            }
1136        "#;
1137
1138        let indexes = parse_sdl(sdl).unwrap();
1139        assert_eq!(indexes.len(), 1);
1140
1141        let index = &indexes[0];
1142        assert_eq!(index.name, "articles");
1143        assert_eq!(index.fields.len(), 2);
1144
1145        assert_eq!(index.fields[0].name, "title");
1146        assert!(matches!(index.fields[0].field_type, FieldType::Text));
1147        assert!(index.fields[0].indexed);
1148        assert!(index.fields[0].stored);
1149
1150        assert_eq!(index.fields[1].name, "body");
1151        assert!(matches!(index.fields[1].field_type, FieldType::Text));
1152        assert!(index.fields[1].indexed);
1153        assert!(!index.fields[1].stored);
1154    }
1155
1156    #[test]
1157    fn test_parse_all_field_types() {
1158        let sdl = r#"
1159            index test {
1160                field text_field: text [indexed, stored]
1161                field u64_field: u64 [indexed, stored]
1162                field i64_field: i64 [indexed, stored]
1163                field f64_field: f64 [indexed, stored]
1164                field bytes_field: bytes [stored]
1165            }
1166        "#;
1167
1168        let indexes = parse_sdl(sdl).unwrap();
1169        let index = &indexes[0];
1170
1171        assert!(matches!(index.fields[0].field_type, FieldType::Text));
1172        assert!(matches!(index.fields[1].field_type, FieldType::U64));
1173        assert!(matches!(index.fields[2].field_type, FieldType::I64));
1174        assert!(matches!(index.fields[3].field_type, FieldType::F64));
1175        assert!(matches!(index.fields[4].field_type, FieldType::Bytes));
1176    }
1177
1178    #[test]
1179    fn test_parse_with_comments() {
1180        let sdl = r#"
1181            # This is a comment
1182            index articles {
1183                # Title field
1184                field title: text [indexed, stored]
1185                field body: text [indexed] # inline comment not supported yet
1186            }
1187        "#;
1188
1189        let indexes = parse_sdl(sdl).unwrap();
1190        assert_eq!(indexes[0].fields.len(), 2);
1191    }
1192
1193    #[test]
1194    fn test_parse_type_aliases() {
1195        let sdl = r#"
1196            index test {
1197                field a: string [indexed]
1198                field b: int [indexed]
1199                field c: uint [indexed]
1200                field d: float [indexed]
1201                field e: binary [stored]
1202            }
1203        "#;
1204
1205        let indexes = parse_sdl(sdl).unwrap();
1206        let index = &indexes[0];
1207
1208        assert!(matches!(index.fields[0].field_type, FieldType::Text));
1209        assert!(matches!(index.fields[1].field_type, FieldType::I64));
1210        assert!(matches!(index.fields[2].field_type, FieldType::U64));
1211        assert!(matches!(index.fields[3].field_type, FieldType::F64));
1212        assert!(matches!(index.fields[4].field_type, FieldType::Bytes));
1213    }
1214
1215    #[test]
1216    fn test_to_schema() {
1217        let sdl = r#"
1218            index articles {
1219                field title: text [indexed, stored]
1220                field views: u64 [indexed, stored]
1221            }
1222        "#;
1223
1224        let indexes = parse_sdl(sdl).unwrap();
1225        let schema = indexes[0].to_schema();
1226
1227        assert!(schema.get_field("title").is_some());
1228        assert!(schema.get_field("views").is_some());
1229        assert!(schema.get_field("nonexistent").is_none());
1230    }
1231
1232    #[test]
1233    fn test_default_attributes() {
1234        let sdl = r#"
1235            index test {
1236                field title: text
1237            }
1238        "#;
1239
1240        let indexes = parse_sdl(sdl).unwrap();
1241        let field = &indexes[0].fields[0];
1242
1243        // Default should be indexed and stored
1244        assert!(field.indexed);
1245        assert!(field.stored);
1246    }
1247
1248    #[test]
1249    fn test_multiple_indexes() {
1250        let sdl = r#"
1251            index articles {
1252                field title: text [indexed, stored]
1253            }
1254
1255            index users {
1256                field name: text [indexed, stored]
1257                field email: text [indexed, stored]
1258            }
1259        "#;
1260
1261        let indexes = parse_sdl(sdl).unwrap();
1262        assert_eq!(indexes.len(), 2);
1263        assert_eq!(indexes[0].name, "articles");
1264        assert_eq!(indexes[1].name, "users");
1265    }
1266
1267    #[test]
1268    fn test_tokenizer_spec() {
1269        let sdl = r#"
1270            index articles {
1271                field title: text<en_stem> [indexed, stored]
1272                field body: text<simple> [indexed]
1273                field author: text [indexed, stored]
1274            }
1275        "#;
1276
1277        let indexes = parse_sdl(sdl).unwrap();
1278        let index = &indexes[0];
1279
1280        assert_eq!(index.fields[0].name, "title");
1281        assert_eq!(index.fields[0].tokenizer, Some("en_stem".to_string()));
1282
1283        assert_eq!(index.fields[1].name, "body");
1284        assert_eq!(index.fields[1].tokenizer, Some("simple".to_string()));
1285
1286        assert_eq!(index.fields[2].name, "author");
1287        assert_eq!(index.fields[2].tokenizer, None); // No tokenizer specified
1288    }
1289
1290    #[test]
1291    fn test_tokenizer_in_schema() {
1292        let sdl = r#"
1293            index articles {
1294                field title: text<german> [indexed, stored]
1295                field body: text<en_stem> [indexed]
1296            }
1297        "#;
1298
1299        let indexes = parse_sdl(sdl).unwrap();
1300        let schema = indexes[0].to_schema();
1301
1302        let title_field = schema.get_field("title").unwrap();
1303        let title_entry = schema.get_field_entry(title_field).unwrap();
1304        assert_eq!(title_entry.tokenizer, Some("german".to_string()));
1305
1306        let body_field = schema.get_field("body").unwrap();
1307        let body_entry = schema.get_field_entry(body_field).unwrap();
1308        assert_eq!(body_entry.tokenizer, Some("en_stem".to_string()));
1309    }
1310
1311    #[test]
1312    fn test_query_router_basic() {
1313        let sdl = r#"
1314            index documents {
1315                field title: text [indexed, stored]
1316                field uri: text [indexed, stored]
1317
1318                query_router {
1319                    pattern: "10\\.\\d{4,}/[^\\s]+"
1320                    substitution: "doi://{0}"
1321                    target_field: uris
1322                    mode: exclusive
1323                }
1324            }
1325        "#;
1326
1327        let indexes = parse_sdl(sdl).unwrap();
1328        let index = &indexes[0];
1329
1330        assert_eq!(index.query_routers.len(), 1);
1331        let router = &index.query_routers[0];
1332        assert_eq!(router.pattern, r"10\.\d{4,}/[^\s]+");
1333        assert_eq!(router.substitution, "doi://{0}");
1334        assert_eq!(router.target_field, "uris");
1335        assert_eq!(router.mode, RoutingMode::Exclusive);
1336    }
1337
1338    #[test]
1339    fn test_query_router_raw_string() {
1340        let sdl = r#"
1341            index documents {
1342                field uris: text [indexed, stored]
1343
1344                query_router {
1345                    pattern: r"^pmid:(\d+)$"
1346                    substitution: "pubmed://{1}"
1347                    target_field: uris
1348                    mode: additional
1349                }
1350            }
1351        "#;
1352
1353        let indexes = parse_sdl(sdl).unwrap();
1354        let router = &indexes[0].query_routers[0];
1355
1356        assert_eq!(router.pattern, r"^pmid:(\d+)$");
1357        assert_eq!(router.substitution, "pubmed://{1}");
1358        assert_eq!(router.mode, RoutingMode::Additional);
1359    }
1360
1361    #[test]
1362    fn test_multiple_query_routers() {
1363        let sdl = r#"
1364            index documents {
1365                field uris: text [indexed, stored]
1366
1367                query_router {
1368                    pattern: r"^doi:(10\.\d{4,}/[^\s]+)$"
1369                    substitution: "doi://{1}"
1370                    target_field: uris
1371                    mode: exclusive
1372                }
1373
1374                query_router {
1375                    pattern: r"^pmid:(\d+)$"
1376                    substitution: "pubmed://{1}"
1377                    target_field: uris
1378                    mode: exclusive
1379                }
1380
1381                query_router {
1382                    pattern: r"^arxiv:(\d+\.\d+)$"
1383                    substitution: "arxiv://{1}"
1384                    target_field: uris
1385                    mode: additional
1386                }
1387            }
1388        "#;
1389
1390        let indexes = parse_sdl(sdl).unwrap();
1391        assert_eq!(indexes[0].query_routers.len(), 3);
1392    }
1393
1394    #[test]
1395    fn test_query_router_default_mode() {
1396        let sdl = r#"
1397            index documents {
1398                field uris: text [indexed, stored]
1399
1400                query_router {
1401                    pattern: r"test"
1402                    substitution: "{0}"
1403                    target_field: uris
1404                }
1405            }
1406        "#;
1407
1408        let indexes = parse_sdl(sdl).unwrap();
1409        // Default mode should be Additional
1410        assert_eq!(indexes[0].query_routers[0].mode, RoutingMode::Additional);
1411    }
1412
1413    #[test]
1414    fn test_multi_attribute() {
1415        let sdl = r#"
1416            index documents {
1417                field uris: text [indexed, stored<multi>]
1418                field title: text [indexed, stored]
1419            }
1420        "#;
1421
1422        let indexes = parse_sdl(sdl).unwrap();
1423        assert_eq!(indexes.len(), 1);
1424
1425        let fields = &indexes[0].fields;
1426        assert_eq!(fields.len(), 2);
1427
1428        // uris should have multi=true
1429        assert_eq!(fields[0].name, "uris");
1430        assert!(fields[0].multi, "uris field should have multi=true");
1431
1432        // title should have multi=false
1433        assert_eq!(fields[1].name, "title");
1434        assert!(!fields[1].multi, "title field should have multi=false");
1435
1436        // Verify schema conversion preserves multi attribute
1437        let schema = indexes[0].to_schema();
1438        let uris_field = schema.get_field("uris").unwrap();
1439        let title_field = schema.get_field("title").unwrap();
1440
1441        assert!(schema.get_field_entry(uris_field).unwrap().multi);
1442        assert!(!schema.get_field_entry(title_field).unwrap().multi);
1443    }
1444
1445    #[test]
1446    fn test_sparse_vector_field() {
1447        let sdl = r#"
1448            index documents {
1449                field embedding: sparse_vector [indexed, stored]
1450            }
1451        "#;
1452
1453        let indexes = parse_sdl(sdl).unwrap();
1454        assert_eq!(indexes.len(), 1);
1455        assert_eq!(indexes[0].fields.len(), 1);
1456        assert_eq!(indexes[0].fields[0].name, "embedding");
1457        assert_eq!(indexes[0].fields[0].field_type, FieldType::SparseVector);
1458        assert!(indexes[0].fields[0].sparse_vector_config.is_none());
1459    }
1460
1461    #[test]
1462    fn test_sparse_vector_with_config() {
1463        let sdl = r#"
1464            index documents {
1465                field embedding: sparse_vector<u16> [indexed<quantization: uint8>, stored]
1466                field dense: sparse_vector<u32> [indexed<quantization: float32>]
1467            }
1468        "#;
1469
1470        let indexes = parse_sdl(sdl).unwrap();
1471        assert_eq!(indexes[0].fields.len(), 2);
1472
1473        // First field: u16 indices, uint8 quantization
1474        let f1 = &indexes[0].fields[0];
1475        assert_eq!(f1.name, "embedding");
1476        let config1 = f1.sparse_vector_config.as_ref().unwrap();
1477        assert_eq!(config1.index_size, IndexSize::U16);
1478        assert_eq!(config1.weight_quantization, WeightQuantization::UInt8);
1479
1480        // Second field: u32 indices, float32 quantization
1481        let f2 = &indexes[0].fields[1];
1482        assert_eq!(f2.name, "dense");
1483        let config2 = f2.sparse_vector_config.as_ref().unwrap();
1484        assert_eq!(config2.index_size, IndexSize::U32);
1485        assert_eq!(config2.weight_quantization, WeightQuantization::Float32);
1486    }
1487
1488    #[test]
1489    fn test_sparse_vector_with_weight_threshold() {
1490        let sdl = r#"
1491            index documents {
1492                field embedding: sparse_vector<u16> [indexed<quantization: uint8, weight_threshold: 0.1>, stored]
1493                field embedding2: sparse_vector<u32> [indexed<quantization: float16, weight_threshold: 0.05>]
1494            }
1495        "#;
1496
1497        let indexes = parse_sdl(sdl).unwrap();
1498        assert_eq!(indexes[0].fields.len(), 2);
1499
1500        // First field: u16 indices, uint8 quantization, threshold 0.1
1501        let f1 = &indexes[0].fields[0];
1502        assert_eq!(f1.name, "embedding");
1503        let config1 = f1.sparse_vector_config.as_ref().unwrap();
1504        assert_eq!(config1.index_size, IndexSize::U16);
1505        assert_eq!(config1.weight_quantization, WeightQuantization::UInt8);
1506        assert!((config1.weight_threshold - 0.1).abs() < 0.001);
1507
1508        // Second field: u32 indices, float16 quantization, threshold 0.05
1509        let f2 = &indexes[0].fields[1];
1510        assert_eq!(f2.name, "embedding2");
1511        let config2 = f2.sparse_vector_config.as_ref().unwrap();
1512        assert_eq!(config2.index_size, IndexSize::U32);
1513        assert_eq!(config2.weight_quantization, WeightQuantization::Float16);
1514        assert!((config2.weight_threshold - 0.05).abs() < 0.001);
1515    }
1516
1517    #[test]
1518    fn test_sparse_vector_with_pruning() {
1519        let sdl = r#"
1520            index documents {
1521                field embedding: sparse_vector [indexed<quantization: uint8, pruning: 0.1>, stored]
1522            }
1523        "#;
1524
1525        let indexes = parse_sdl(sdl).unwrap();
1526        let f = &indexes[0].fields[0];
1527        assert_eq!(f.name, "embedding");
1528        let config = f.sparse_vector_config.as_ref().unwrap();
1529        assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
1530        assert_eq!(config.pruning, Some(0.1));
1531    }
1532
1533    #[test]
1534    fn test_dense_vector_field() {
1535        let sdl = r#"
1536            index documents {
1537                field embedding: dense_vector<768> [indexed, stored]
1538            }
1539        "#;
1540
1541        let indexes = parse_sdl(sdl).unwrap();
1542        assert_eq!(indexes.len(), 1);
1543        assert_eq!(indexes[0].fields.len(), 1);
1544
1545        let f = &indexes[0].fields[0];
1546        assert_eq!(f.name, "embedding");
1547        assert_eq!(f.field_type, FieldType::DenseVector);
1548
1549        let config = f.dense_vector_config.as_ref().unwrap();
1550        assert_eq!(config.dim, 768);
1551    }
1552
1553    #[test]
1554    fn test_dense_vector_alias() {
1555        let sdl = r#"
1556            index documents {
1557                field embedding: vector<1536> [indexed]
1558            }
1559        "#;
1560
1561        let indexes = parse_sdl(sdl).unwrap();
1562        assert_eq!(indexes[0].fields[0].field_type, FieldType::DenseVector);
1563        assert_eq!(
1564            indexes[0].fields[0]
1565                .dense_vector_config
1566                .as_ref()
1567                .unwrap()
1568                .dim,
1569            1536
1570        );
1571    }
1572
1573    #[test]
1574    fn test_dense_vector_with_num_clusters() {
1575        let sdl = r#"
1576            index documents {
1577                field embedding: dense_vector<768> [indexed<ivf_rabitq, num_clusters: 256>, stored]
1578            }
1579        "#;
1580
1581        let indexes = parse_sdl(sdl).unwrap();
1582        assert_eq!(indexes.len(), 1);
1583
1584        let f = &indexes[0].fields[0];
1585        assert_eq!(f.name, "embedding");
1586        assert_eq!(f.field_type, FieldType::DenseVector);
1587
1588        let config = f.dense_vector_config.as_ref().unwrap();
1589        assert_eq!(config.dim, 768);
1590        assert_eq!(config.num_clusters, Some(256));
1591        assert_eq!(config.nprobe, 32); // default
1592    }
1593
1594    #[test]
1595    fn test_dense_vector_with_soar() {
1596        let sdl = r#"
1597            index documents {
1598                field embedding: dense_vector<768> [indexed<ivf_rabitq, num_clusters: 256, soar: selective>, stored]
1599            }
1600        "#;
1601
1602        let indexes = parse_sdl(sdl).unwrap();
1603        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1604
1605        let soar = config.soar.as_ref().expect("soar should be enabled");
1606        assert_eq!(soar.num_secondary, 1);
1607        assert!(soar.selective);
1608
1609        // aggressive preset: 2 secondary clusters, no selectivity
1610        let sdl = r#"
1611            index documents {
1612                field embedding: dense_vector<768> [indexed<scann, soar: aggressive>]
1613            }
1614        "#;
1615        let indexes = parse_sdl(sdl).unwrap();
1616        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1617        let soar = config.soar.as_ref().expect("soar should be enabled");
1618        assert_eq!(soar.num_secondary, 2);
1619        assert!(!soar.selective);
1620
1621        // off keeps soar disabled
1622        let sdl = r#"
1623            index documents {
1624                field embedding: dense_vector<768> [indexed<ivf_rabitq, soar: off>]
1625            }
1626        "#;
1627        let indexes = parse_sdl(sdl).unwrap();
1628        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1629        assert!(config.soar.is_none());
1630    }
1631
1632    #[test]
1633    fn test_dense_vector_with_num_clusters_and_nprobe() {
1634        let sdl = r#"
1635            index documents {
1636                field embedding: dense_vector<1536> [indexed<ivf_rabitq, num_clusters: 512, nprobe: 64>]
1637            }
1638        "#;
1639
1640        let indexes = parse_sdl(sdl).unwrap();
1641        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1642
1643        assert_eq!(config.dim, 1536);
1644        assert_eq!(config.num_clusters, Some(512));
1645        assert_eq!(config.nprobe, 64);
1646    }
1647
1648    #[test]
1649    fn test_dense_vector_keyword_syntax() {
1650        let sdl = r#"
1651            index documents {
1652                field embedding: dense_vector<dims: 1536> [indexed, stored]
1653            }
1654        "#;
1655
1656        let indexes = parse_sdl(sdl).unwrap();
1657        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1658
1659        assert_eq!(config.dim, 1536);
1660        assert!(config.num_clusters.is_none());
1661    }
1662
1663    #[test]
1664    fn test_dense_vector_keyword_syntax_full() {
1665        let sdl = r#"
1666            index documents {
1667                field embedding: dense_vector<dims: 1536> [indexed<ivf_rabitq, num_clusters: 256, nprobe: 64>]
1668            }
1669        "#;
1670
1671        let indexes = parse_sdl(sdl).unwrap();
1672        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1673
1674        assert_eq!(config.dim, 1536);
1675        assert_eq!(config.num_clusters, Some(256));
1676        assert_eq!(config.nprobe, 64);
1677    }
1678
1679    #[test]
1680    fn test_dense_vector_keyword_syntax_partial() {
1681        let sdl = r#"
1682            index documents {
1683                field embedding: dense_vector<dims: 768> [indexed<ivf_rabitq, num_clusters: 128>]
1684            }
1685        "#;
1686
1687        let indexes = parse_sdl(sdl).unwrap();
1688        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1689
1690        assert_eq!(config.dim, 768);
1691        assert_eq!(config.num_clusters, Some(128));
1692        assert_eq!(config.nprobe, 32); // default
1693    }
1694
1695    #[test]
1696    fn test_dense_vector_scann_index() {
1697        use crate::dsl::schema::VectorIndexType;
1698
1699        let sdl = r#"
1700            index documents {
1701                field embedding: dense_vector<dims: 768> [indexed<scann, num_clusters: 256, nprobe: 64>]
1702            }
1703        "#;
1704
1705        let indexes = parse_sdl(sdl).unwrap();
1706        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1707
1708        assert_eq!(config.dim, 768);
1709        assert_eq!(config.index_type, VectorIndexType::ScaNN);
1710        assert_eq!(config.num_clusters, Some(256));
1711        assert_eq!(config.nprobe, 64);
1712    }
1713
1714    #[test]
1715    fn test_dense_vector_ivf_rabitq_index() {
1716        use crate::dsl::schema::VectorIndexType;
1717
1718        let sdl = r#"
1719            index documents {
1720                field embedding: dense_vector<dims: 1536> [indexed<ivf_rabitq, num_clusters: 512>]
1721            }
1722        "#;
1723
1724        let indexes = parse_sdl(sdl).unwrap();
1725        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1726
1727        assert_eq!(config.dim, 1536);
1728        assert_eq!(config.index_type, VectorIndexType::IvfRaBitQ);
1729        assert_eq!(config.num_clusters, Some(512));
1730    }
1731
1732    #[test]
1733    fn test_dense_vector_rabitq_no_clusters() {
1734        use crate::dsl::schema::VectorIndexType;
1735
1736        let sdl = r#"
1737            index documents {
1738                field embedding: dense_vector<dims: 768> [indexed<rabitq>]
1739            }
1740        "#;
1741
1742        let indexes = parse_sdl(sdl).unwrap();
1743        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1744
1745        assert_eq!(config.dim, 768);
1746        assert_eq!(config.index_type, VectorIndexType::RaBitQ);
1747        assert!(config.num_clusters.is_none());
1748    }
1749
1750    #[test]
1751    fn test_dense_vector_flat_index() {
1752        use crate::dsl::schema::VectorIndexType;
1753
1754        let sdl = r#"
1755            index documents {
1756                field embedding: dense_vector<dims: 768> [indexed<flat>]
1757            }
1758        "#;
1759
1760        let indexes = parse_sdl(sdl).unwrap();
1761        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1762
1763        assert_eq!(config.dim, 768);
1764        assert_eq!(config.index_type, VectorIndexType::Flat);
1765    }
1766
1767    #[test]
1768    fn test_dense_vector_default_index_type() {
1769        use crate::dsl::schema::VectorIndexType;
1770
1771        // When no index type specified, should default to RaBitQ (basic)
1772        let sdl = r#"
1773            index documents {
1774                field embedding: dense_vector<dims: 768> [indexed]
1775            }
1776        "#;
1777
1778        let indexes = parse_sdl(sdl).unwrap();
1779        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1780
1781        assert_eq!(config.dim, 768);
1782        assert_eq!(config.index_type, VectorIndexType::RaBitQ);
1783    }
1784
1785    #[test]
1786    fn test_dense_vector_f16_quantization() {
1787        use crate::dsl::schema::{DenseVectorQuantization, VectorIndexType};
1788
1789        let sdl = r#"
1790            index documents {
1791                field embedding: dense_vector<768, f16> [indexed]
1792            }
1793        "#;
1794
1795        let indexes = parse_sdl(sdl).unwrap();
1796        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1797
1798        assert_eq!(config.dim, 768);
1799        assert_eq!(config.quantization, DenseVectorQuantization::F16);
1800        assert_eq!(config.index_type, VectorIndexType::RaBitQ);
1801    }
1802
1803    #[test]
1804    fn test_dense_vector_uint8_quantization() {
1805        use crate::dsl::schema::DenseVectorQuantization;
1806
1807        let sdl = r#"
1808            index documents {
1809                field embedding: dense_vector<1024, uint8> [indexed<ivf_rabitq>]
1810            }
1811        "#;
1812
1813        let indexes = parse_sdl(sdl).unwrap();
1814        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1815
1816        assert_eq!(config.dim, 1024);
1817        assert_eq!(config.quantization, DenseVectorQuantization::UInt8);
1818    }
1819
1820    #[test]
1821    fn test_dense_vector_u8_alias() {
1822        use crate::dsl::schema::DenseVectorQuantization;
1823
1824        let sdl = r#"
1825            index documents {
1826                field embedding: dense_vector<512, u8> [indexed]
1827            }
1828        "#;
1829
1830        let indexes = parse_sdl(sdl).unwrap();
1831        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1832
1833        assert_eq!(config.dim, 512);
1834        assert_eq!(config.quantization, DenseVectorQuantization::UInt8);
1835    }
1836
1837    #[test]
1838    fn test_dense_vector_default_f32_quantization() {
1839        use crate::dsl::schema::DenseVectorQuantization;
1840
1841        // No quantization type → default f32
1842        let sdl = r#"
1843            index documents {
1844                field embedding: dense_vector<768> [indexed]
1845            }
1846        "#;
1847
1848        let indexes = parse_sdl(sdl).unwrap();
1849        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1850
1851        assert_eq!(config.dim, 768);
1852        assert_eq!(config.quantization, DenseVectorQuantization::F32);
1853    }
1854
1855    #[test]
1856    fn test_dense_vector_keyword_with_quantization() {
1857        use crate::dsl::schema::DenseVectorQuantization;
1858
1859        let sdl = r#"
1860            index documents {
1861                field embedding: dense_vector<dims: 768, f16> [indexed]
1862            }
1863        "#;
1864
1865        let indexes = parse_sdl(sdl).unwrap();
1866        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1867
1868        assert_eq!(config.dim, 768);
1869        assert_eq!(config.quantization, DenseVectorQuantization::F16);
1870    }
1871
1872    #[test]
1873    fn test_json_field_type() {
1874        let sdl = r#"
1875            index documents {
1876                field title: text [indexed, stored]
1877                field metadata: json [stored]
1878                field extra: json
1879            }
1880        "#;
1881
1882        let indexes = parse_sdl(sdl).unwrap();
1883        let index = &indexes[0];
1884
1885        assert_eq!(index.fields.len(), 3);
1886
1887        // Check JSON field
1888        assert_eq!(index.fields[1].name, "metadata");
1889        assert!(matches!(index.fields[1].field_type, FieldType::Json));
1890        assert!(index.fields[1].stored);
1891        // JSON fields should not be indexed (enforced by add_json_field)
1892
1893        // Check default attributes for JSON field
1894        assert_eq!(index.fields[2].name, "extra");
1895        assert!(matches!(index.fields[2].field_type, FieldType::Json));
1896
1897        // Verify schema conversion
1898        let schema = index.to_schema();
1899        let metadata_field = schema.get_field("metadata").unwrap();
1900        let entry = schema.get_field_entry(metadata_field).unwrap();
1901        assert_eq!(entry.field_type, FieldType::Json);
1902        assert!(!entry.indexed); // JSON fields are never indexed
1903        assert!(entry.stored);
1904    }
1905
1906    #[test]
1907    fn test_sparse_vector_query_config() {
1908        use crate::structures::QueryWeighting;
1909
1910        let sdl = r#"
1911            index documents {
1912                field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<tokenizer: "Alibaba-NLP/gte-Qwen2-1.5B-instruct", weighting: idf>>]
1913            }
1914        "#;
1915
1916        let indexes = parse_sdl(sdl).unwrap();
1917        let index = &indexes[0];
1918
1919        assert_eq!(index.fields.len(), 1);
1920        assert_eq!(index.fields[0].name, "embedding");
1921        assert!(matches!(
1922            index.fields[0].field_type,
1923            FieldType::SparseVector
1924        ));
1925
1926        let config = index.fields[0].sparse_vector_config.as_ref().unwrap();
1927        assert_eq!(config.index_size, IndexSize::U16);
1928        assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
1929
1930        // Check query config
1931        let query_config = config.query_config.as_ref().unwrap();
1932        assert_eq!(
1933            query_config.tokenizer.as_deref(),
1934            Some("Alibaba-NLP/gte-Qwen2-1.5B-instruct")
1935        );
1936        assert_eq!(query_config.weighting, QueryWeighting::Idf);
1937
1938        // Verify schema conversion preserves query config
1939        let schema = index.to_schema();
1940        let embedding_field = schema.get_field("embedding").unwrap();
1941        let entry = schema.get_field_entry(embedding_field).unwrap();
1942        let sv_config = entry.sparse_vector_config.as_ref().unwrap();
1943        let qc = sv_config.query_config.as_ref().unwrap();
1944        assert_eq!(
1945            qc.tokenizer.as_deref(),
1946            Some("Alibaba-NLP/gte-Qwen2-1.5B-instruct")
1947        );
1948        assert_eq!(qc.weighting, QueryWeighting::Idf);
1949    }
1950
1951    #[test]
1952    fn test_sparse_vector_query_config_weighting_one() {
1953        use crate::structures::QueryWeighting;
1954
1955        let sdl = r#"
1956            index documents {
1957                field embedding: sparse_vector [indexed<query<weighting: one>>]
1958            }
1959        "#;
1960
1961        let indexes = parse_sdl(sdl).unwrap();
1962        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
1963
1964        let query_config = config.query_config.as_ref().unwrap();
1965        assert!(query_config.tokenizer.is_none());
1966        assert_eq!(query_config.weighting, QueryWeighting::One);
1967    }
1968
1969    #[test]
1970    fn test_sparse_vector_query_config_weighting_idf_file() {
1971        use crate::structures::QueryWeighting;
1972
1973        let sdl = r#"
1974            index documents {
1975                field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<tokenizer: "opensearch-neural-sparse-encoding-v1", weighting: idf_file>>]
1976            }
1977        "#;
1978
1979        let indexes = parse_sdl(sdl).unwrap();
1980        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
1981
1982        let query_config = config.query_config.as_ref().unwrap();
1983        assert_eq!(
1984            query_config.tokenizer.as_deref(),
1985            Some("opensearch-neural-sparse-encoding-v1")
1986        );
1987        assert_eq!(query_config.weighting, QueryWeighting::IdfFile);
1988
1989        // Verify schema conversion preserves idf_file
1990        let schema = indexes[0].to_schema();
1991        let field = schema.get_field("embedding").unwrap();
1992        let entry = schema.get_field_entry(field).unwrap();
1993        let sc = entry.sparse_vector_config.as_ref().unwrap();
1994        let qc = sc.query_config.as_ref().unwrap();
1995        assert_eq!(qc.weighting, QueryWeighting::IdfFile);
1996    }
1997
1998    #[test]
1999    fn test_sparse_vector_query_config_pruning_params() {
2000        let sdl = r#"
2001            index documents {
2002                field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<weighting: idf, weight_threshold: 0.03, max_dims: 25, pruning: 0.2>>]
2003            }
2004        "#;
2005
2006        let indexes = parse_sdl(sdl).unwrap();
2007        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2008
2009        let qc = config.query_config.as_ref().unwrap();
2010        assert_eq!(qc.weighting, QueryWeighting::Idf);
2011        assert!((qc.weight_threshold - 0.03).abs() < 0.001);
2012        assert_eq!(qc.max_query_dims, Some(25));
2013        assert!((qc.pruning.unwrap() - 0.2).abs() < 0.001);
2014
2015        // Verify schema roundtrip
2016        let schema = indexes[0].to_schema();
2017        let field = schema.get_field("embedding").unwrap();
2018        let entry = schema.get_field_entry(field).unwrap();
2019        let sc = entry.sparse_vector_config.as_ref().unwrap();
2020        let rqc = sc.query_config.as_ref().unwrap();
2021        assert!((rqc.weight_threshold - 0.03).abs() < 0.001);
2022        assert_eq!(rqc.max_query_dims, Some(25));
2023        assert!((rqc.pruning.unwrap() - 0.2).abs() < 0.001);
2024    }
2025
2026    #[test]
2027    fn test_sparse_vector_format_maxscore() {
2028        let sdl = r#"
2029            index documents {
2030                field embedding: sparse_vector<u16> [indexed<format: maxscore, quantization: uint8>]
2031            }
2032        "#;
2033
2034        let indexes = parse_sdl(sdl).unwrap();
2035        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2036        assert_eq!(config.format, SparseFormat::MaxScore);
2037        assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
2038
2039        // Verify schema roundtrip
2040        let schema = indexes[0].to_schema();
2041        let field = schema.get_field("embedding").unwrap();
2042        let entry = schema.get_field_entry(field).unwrap();
2043        let sc = entry.sparse_vector_config.as_ref().unwrap();
2044        assert_eq!(sc.format, SparseFormat::MaxScore);
2045    }
2046
2047    #[test]
2048    fn test_sparse_vector_format_bmp() {
2049        let sdl = r#"
2050            index documents {
2051                field embedding: sparse_vector<u16> [indexed<format: bmp, quantization: uint8>]
2052            }
2053        "#;
2054
2055        let indexes = parse_sdl(sdl).unwrap();
2056        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2057        assert_eq!(config.format, SparseFormat::Bmp);
2058    }
2059
2060    #[test]
2061    fn test_fast_attribute() {
2062        let sdl = r#"
2063            index products {
2064                field name: text [indexed, stored]
2065                field price: f64 [indexed, fast]
2066                field category: text [indexed, stored, fast]
2067                field count: u64 [fast]
2068                field score: i64 [indexed, stored, fast]
2069            }
2070        "#;
2071
2072        let indexes = parse_sdl(sdl).unwrap();
2073        assert_eq!(indexes.len(), 1);
2074        let index = &indexes[0];
2075        assert_eq!(index.fields.len(), 5);
2076
2077        // name: no fast
2078        assert!(!index.fields[0].fast);
2079        // price: fast
2080        assert!(index.fields[1].fast);
2081        assert!(matches!(index.fields[1].field_type, FieldType::F64));
2082        // category: fast text
2083        assert!(index.fields[2].fast);
2084        assert!(matches!(index.fields[2].field_type, FieldType::Text));
2085        // count: fast only
2086        assert!(index.fields[3].fast);
2087        assert!(matches!(index.fields[3].field_type, FieldType::U64));
2088        // score: fast i64
2089        assert!(index.fields[4].fast);
2090        assert!(matches!(index.fields[4].field_type, FieldType::I64));
2091
2092        // Verify schema roundtrip preserves fast flag
2093        let schema = index.to_schema();
2094        let price_field = schema.get_field("price").unwrap();
2095        assert!(schema.get_field_entry(price_field).unwrap().fast);
2096
2097        let category_field = schema.get_field("category").unwrap();
2098        assert!(schema.get_field_entry(category_field).unwrap().fast);
2099
2100        let name_field = schema.get_field("name").unwrap();
2101        assert!(!schema.get_field_entry(name_field).unwrap().fast);
2102    }
2103
2104    #[test]
2105    fn test_primary_attribute() {
2106        let sdl = r#"
2107            index documents {
2108                field id: text [primary, stored]
2109                field title: text [indexed, stored]
2110            }
2111        "#;
2112
2113        let indexes = parse_sdl(sdl).unwrap();
2114        assert_eq!(indexes.len(), 1);
2115        let index = &indexes[0];
2116        assert_eq!(index.fields.len(), 2);
2117
2118        // id should be primary, and auto-set fast + indexed
2119        let id_field = &index.fields[0];
2120        assert!(id_field.primary, "id should be primary");
2121        assert!(id_field.fast, "primary implies fast");
2122        assert!(id_field.indexed, "primary implies indexed");
2123
2124        // title should NOT be primary
2125        assert!(!index.fields[1].primary);
2126
2127        // Verify schema conversion preserves primary_key
2128        let schema = index.to_schema();
2129        let id = schema.get_field("id").unwrap();
2130        let id_entry = schema.get_field_entry(id).unwrap();
2131        assert!(id_entry.primary_key);
2132        assert!(id_entry.fast);
2133        assert!(id_entry.indexed);
2134
2135        let title = schema.get_field("title").unwrap();
2136        assert!(!schema.get_field_entry(title).unwrap().primary_key);
2137
2138        // primary_field() should return the primary field
2139        assert_eq!(schema.primary_field(), Some(id));
2140    }
2141
2142    #[test]
2143    fn test_primary_with_other_attributes() {
2144        let sdl = r#"
2145            index documents {
2146                field id: text<simple> [primary, indexed, stored]
2147                field body: text [indexed]
2148            }
2149        "#;
2150
2151        let indexes = parse_sdl(sdl).unwrap();
2152        let id_field = &indexes[0].fields[0];
2153        assert!(id_field.primary);
2154        assert!(id_field.indexed);
2155        assert!(id_field.stored);
2156        assert!(id_field.fast);
2157        assert_eq!(id_field.tokenizer, Some("simple".to_string()));
2158    }
2159
2160    #[test]
2161    fn test_primary_only_one_allowed() {
2162        let sdl = r#"
2163            index documents {
2164                field id: text [primary]
2165                field alt_id: text [primary]
2166            }
2167        "#;
2168
2169        let result = parse_sdl(sdl);
2170        assert!(result.is_err());
2171        let err = result.unwrap_err().to_string();
2172        assert!(
2173            err.contains("primary key"),
2174            "Error should mention primary key: {}",
2175            err
2176        );
2177    }
2178
2179    #[test]
2180    fn test_primary_must_be_text() {
2181        let sdl = r#"
2182            index documents {
2183                field id: u64 [primary]
2184            }
2185        "#;
2186
2187        let result = parse_sdl(sdl);
2188        assert!(result.is_err());
2189        let err = result.unwrap_err().to_string();
2190        assert!(
2191            err.contains("text"),
2192            "Error should mention text type: {}",
2193            err
2194        );
2195    }
2196
2197    #[test]
2198    fn test_primary_cannot_be_multi() {
2199        let sdl = r#"
2200            index documents {
2201                field id: text [primary, stored<multi>]
2202            }
2203        "#;
2204
2205        let result = parse_sdl(sdl);
2206        assert!(result.is_err());
2207        let err = result.unwrap_err().to_string();
2208        assert!(err.contains("multi"), "Error should mention multi: {}", err);
2209    }
2210
2211    #[test]
2212    fn test_no_primary_field() {
2213        // Schema without primary field should work fine
2214        let sdl = r#"
2215            index documents {
2216                field title: text [indexed, stored]
2217            }
2218        "#;
2219
2220        let indexes = parse_sdl(sdl).unwrap();
2221        let schema = indexes[0].to_schema();
2222        assert!(schema.primary_field().is_none());
2223    }
2224
2225    #[test]
2226    fn test_reorder_attribute() {
2227        let sdl = r#"
2228            index documents {
2229                field embedding: sparse_vector<u16> [indexed<format: bmp, quantization: uint8>, reorder]
2230                field embedding2: sparse_vector [indexed<format: bmp>]
2231            }
2232        "#;
2233
2234        let indexes = parse_sdl(sdl).unwrap();
2235        assert_eq!(indexes[0].fields.len(), 2);
2236
2237        // First field should have reorder=true
2238        assert!(indexes[0].fields[0].reorder);
2239        // Second field should have reorder=false
2240        assert!(!indexes[0].fields[1].reorder);
2241
2242        // Verify schema roundtrip
2243        let schema = indexes[0].to_schema();
2244        let f1 = schema.get_field("embedding").unwrap();
2245        assert!(schema.get_field_entry(f1).unwrap().reorder);
2246
2247        let f2 = schema.get_field("embedding2").unwrap();
2248        assert!(!schema.get_field_entry(f2).unwrap().reorder);
2249    }
2250}