1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3
4use crate::constraint::TableConstraints;
5use crate::error::{MongrelError, Result};
6use crate::memtable::Value;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(tag = "kind", rename_all = "lowercase")]
12pub enum TypeId {
13 Bool,
14 Int8,
15 Int16,
16 Int32,
17 Int64,
18 UInt8,
19 UInt16,
20 UInt32,
21 UInt64,
22 Float32,
23 Float64,
24 TimestampNanos,
25 Date32,
26 Date64,
29 Time64,
31 Interval,
34 Uuid,
36 Json,
39 Array {
44 element_type: u8,
45 },
46 Bytes,
48 Embedding {
50 dim: u32,
51 },
52 Decimal128 {
55 precision: u8,
56 scale: i8,
57 },
58 Enum {
64 variants: Arc<[String]>,
65 },
66}
67
68impl TypeId {
69 pub fn fixed_size(&self) -> Option<usize> {
71 match self {
72 TypeId::Bool => Some(1),
73 TypeId::Int8 | TypeId::UInt8 => Some(1),
74 TypeId::Int16 | TypeId::UInt16 => Some(2),
75 TypeId::Int32 | TypeId::UInt32 | TypeId::Float32 | TypeId::Date32 => Some(4),
76 TypeId::Int64
77 | TypeId::UInt64
78 | TypeId::Float64
79 | TypeId::TimestampNanos
80 | TypeId::Date64
81 | TypeId::Time64 => Some(8),
82 TypeId::Bytes | TypeId::Embedding { .. } | TypeId::Enum { .. } => None,
83 TypeId::Decimal128 { .. } => Some(16),
84 TypeId::Uuid => Some(16),
85 TypeId::Json | TypeId::Array { .. } => None,
86 TypeId::Interval => Some(20), }
88 }
89}
90
91#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
93pub struct ColumnFlags {
94 bits: u32,
95}
96
97impl ColumnFlags {
98 pub const NULLABLE: u32 = 1 << 0;
99 pub const PRIMARY_KEY: u32 = 1 << 1;
100 pub const ENCRYPTED: u32 = 1 << 2;
101 pub const ENCRYPTED_INDEXABLE: u32 = 1 << 3;
104 pub const EMBEDDING_BINARY_QUANTIZED: u32 = 1 << 4;
106 pub const AUTO_INCREMENT: u32 = 1 << 5;
113
114 #[inline]
115 pub const fn empty() -> Self {
116 Self { bits: 0 }
117 }
118
119 #[inline]
120 pub const fn with(mut self, flag: u32) -> Self {
121 self.bits |= flag;
122 self
123 }
124
125 #[inline]
126 pub const fn without(mut self, flag: u32) -> Self {
127 self.bits &= !flag;
128 self
129 }
130
131 #[inline]
132 pub const fn contains(&self, flag: u32) -> bool {
133 self.bits & flag != 0
134 }
135
136 #[inline]
137 pub const fn bits(&self) -> u32 {
138 self.bits
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub enum DefaultExpr {
148 Static(Value),
150 Now,
153 Uuid,
155}
156
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct ColumnDef {
160 pub id: u16,
161 pub name: String,
162 pub ty: TypeId,
163 pub flags: ColumnFlags,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub default_value: Option<DefaultExpr>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub embedding_source: Option<crate::embedding::EmbeddingSource>,
177}
178
179#[derive(Debug, Clone, Default, Serialize, Deserialize)]
181pub struct AlterColumn {
182 pub name: Option<String>,
183 pub ty: Option<TypeId>,
184 pub flags: Option<ColumnFlags>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub default_value: Option<Option<DefaultExpr>>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub embedding_source: Option<Option<crate::embedding::EmbeddingSource>>,
193}
194
195impl AlterColumn {
196 pub fn rename(name: impl Into<String>) -> Self {
197 Self {
198 name: Some(name.into()),
199 ty: None,
200 flags: None,
201 default_value: None,
202 embedding_source: None,
203 }
204 }
205
206 pub fn set_type(ty: TypeId) -> Self {
207 Self {
208 name: None,
209 ty: Some(ty),
210 flags: None,
211 default_value: None,
212 embedding_source: None,
213 }
214 }
215
216 pub fn set_flags(flags: ColumnFlags) -> Self {
217 Self {
218 name: None,
219 ty: None,
220 flags: Some(flags),
221 default_value: None,
222 embedding_source: None,
223 }
224 }
225
226 pub fn set_default(expr: DefaultExpr) -> Self {
227 Self {
228 name: None,
229 ty: None,
230 flags: None,
231 default_value: Some(Some(expr)),
232 embedding_source: None,
233 }
234 }
235
236 pub fn drop_default() -> Self {
237 Self {
238 name: None,
239 ty: None,
240 flags: None,
241 default_value: Some(None),
242 embedding_source: None,
243 }
244 }
245
246 pub fn set_embedding_source(source: crate::embedding::EmbeddingSource) -> Self {
248 Self {
249 name: None,
250 ty: None,
251 flags: None,
252 default_value: None,
253 embedding_source: Some(Some(source)),
254 }
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
261pub enum IndexKind {
262 Bitmap,
264 FmIndex,
266 Ann,
268 LearnedRange,
270 MinHash,
272 Sparse,
274}
275
276#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
277pub struct IndexOptions {
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub ann: Option<AnnOptions>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub minhash: Option<MinHashOptions>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub learned_range: Option<LearnedRangeOptions>,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287pub struct AnnOptions {
288 #[serde(default = "default_ann_m")]
289 pub m: usize,
290 #[serde(default = "default_ann_ef_construction")]
291 pub ef_construction: usize,
292 #[serde(default = "default_ann_ef_search")]
293 pub ef_search: usize,
294 #[serde(default)]
295 pub quantization: AnnQuantization,
296 #[serde(default)]
301 pub algorithm: AnnAlgorithm,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub diskann: Option<DiskAnnOptions>,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub ivf: Option<IvfOptions>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub product: Option<ProductQuantizerOptions>,
314}
315
316impl Default for AnnOptions {
317 fn default() -> Self {
318 Self {
319 m: default_ann_m(),
320 ef_construction: default_ann_ef_construction(),
321 ef_search: default_ann_ef_search(),
322 quantization: AnnQuantization::BinarySign,
323 algorithm: AnnAlgorithm::default(),
324 diskann: None,
325 ivf: None,
326 product: None,
327 }
328 }
329}
330
331#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335pub enum AnnAlgorithm {
336 #[default]
339 Hnsw,
340 DiskAnn,
343 Ivf,
346}
347
348#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
352#[serde(rename_all = "snake_case")]
353pub enum AnnQuantization {
354 #[default]
355 BinarySign,
356 Dense,
358 Product {
364 num_subvectors: u16,
366 bits: u8,
369 },
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
374pub struct DiskAnnOptions {
375 #[serde(default = "default_diskann_r")]
377 pub r: usize,
378 #[serde(default = "default_diskann_l")]
381 pub l: usize,
382 #[serde(default = "default_diskann_beam_width")]
385 pub beam_width: usize,
386 #[serde(default = "default_diskann_alpha")]
389 pub alpha: u32,
390}
391
392impl Default for DiskAnnOptions {
393 fn default() -> Self {
394 Self {
395 r: default_diskann_r(),
396 l: default_diskann_l(),
397 beam_width: default_diskann_beam_width(),
398 alpha: default_diskann_alpha(),
399 }
400 }
401}
402
403const fn default_diskann_r() -> usize {
404 64
405}
406const fn default_diskann_l() -> usize {
407 128
408}
409const fn default_diskann_beam_width() -> usize {
410 8
411}
412const fn default_diskann_alpha() -> u32 {
413 120
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
418pub struct IvfOptions {
419 #[serde(default = "default_ivf_nlist")]
421 pub nlist: usize,
422 #[serde(default = "default_ivf_nprobe")]
424 pub nprobe: usize,
425}
426
427impl Default for IvfOptions {
428 fn default() -> Self {
429 Self {
430 nlist: default_ivf_nlist(),
431 nprobe: default_ivf_nprobe(),
432 }
433 }
434}
435
436const fn default_ivf_nlist() -> usize {
437 256
438}
439const fn default_ivf_nprobe() -> usize {
440 8
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447pub struct ProductQuantizerOptions {
448 #[serde(default = "default_pq_training_samples")]
451 pub training_samples: usize,
452 #[serde(default = "default_pq_seed")]
455 pub seed: u64,
456 #[serde(default = "default_pq_rerank_factor")]
460 pub rerank_factor: usize,
461}
462
463impl Default for ProductQuantizerOptions {
464 fn default() -> Self {
465 Self {
466 training_samples: default_pq_training_samples(),
467 seed: default_pq_seed(),
468 rerank_factor: default_pq_rerank_factor(),
469 }
470 }
471}
472
473const fn default_pq_training_samples() -> usize {
474 256_000
475}
476const fn default_pq_seed() -> u64 {
477 0x9E37_79B9_7F4A_7C15
478}
479const fn default_pq_rerank_factor() -> usize {
480 5
481}
482
483const fn default_ann_m() -> usize {
484 16
485}
486const fn default_ann_ef_construction() -> usize {
487 64
488}
489const fn default_ann_ef_search() -> usize {
490 64
491}
492
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
494pub struct MinHashOptions {
495 #[serde(default = "default_minhash_permutations")]
496 pub permutations: usize,
497 #[serde(default = "default_minhash_bands")]
498 pub bands: usize,
499}
500
501impl Default for MinHashOptions {
502 fn default() -> Self {
503 Self {
504 permutations: default_minhash_permutations(),
505 bands: default_minhash_bands(),
506 }
507 }
508}
509
510const fn default_minhash_permutations() -> usize {
511 128
512}
513const fn default_minhash_bands() -> usize {
514 32
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
518pub struct LearnedRangeOptions {
519 #[serde(default = "default_learned_range_epsilon")]
520 pub epsilon: usize,
521}
522
523impl Default for LearnedRangeOptions {
524 fn default() -> Self {
525 Self {
526 epsilon: default_learned_range_epsilon(),
527 }
528 }
529}
530
531const fn default_learned_range_epsilon() -> usize {
532 16
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536pub struct IndexDef {
537 pub name: String,
538 pub column_id: u16,
539 pub kind: IndexKind,
540 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub predicate: Option<String>,
545 #[serde(default)]
546 pub options: IndexOptions,
547}
548
549impl IndexDef {
550 pub fn validate_options(&self) -> Result<()> {
551 if self.options.ann.is_some() && self.kind != IndexKind::Ann
552 || self.options.minhash.is_some() && self.kind != IndexKind::MinHash
553 || self.options.learned_range.is_some() && self.kind != IndexKind::LearnedRange
554 {
555 return Err(MongrelError::Schema(format!(
556 "index {} has options for a different index kind",
557 self.name
558 )));
559 }
560 if let Some(options) = &self.options.ann {
561 if options.m == 0
562 || options.ef_construction < options.m
563 || options.ef_search == 0
564 || options.m > 256
565 || options.ef_construction > 65_536
566 || options.ef_search > 65_536
567 {
568 return Err(MongrelError::Schema(format!(
569 "invalid ANN options for index {}",
570 self.name
571 )));
572 }
573 if let Some(diskann) = &options.diskann {
576 if diskann.r == 0
577 || diskann.l < diskann.r
578 || diskann.r > 1024
579 || diskann.l > 1_048_576
580 || diskann.beam_width == 0
581 || diskann.beam_width > 1024
582 || !(100..=300).contains(&diskann.alpha)
583 {
584 return Err(MongrelError::Schema(format!(
585 "invalid DiskANN options for index {}",
586 self.name
587 )));
588 }
589 }
590 if let Some(ivf) = &options.ivf {
591 if ivf.nlist == 0
592 || ivf.nprobe == 0
593 || ivf.nprobe > ivf.nlist
594 || ivf.nlist > 1_048_576
595 {
596 return Err(MongrelError::Schema(format!(
597 "invalid IVF options for index {}",
598 self.name
599 )));
600 }
601 }
602 if options.diskann.is_some() && options.algorithm != AnnAlgorithm::DiskAnn {
606 return Err(MongrelError::Schema(format!(
607 "DiskANN options supplied for non-DiskANN algorithm on index {}",
608 self.name
609 )));
610 }
611 if options.ivf.is_some() && options.algorithm != AnnAlgorithm::Ivf {
612 return Err(MongrelError::Schema(format!(
613 "IVF options supplied for non-IVF algorithm on index {}",
614 self.name
615 )));
616 }
617 if options.product.is_some()
618 && !matches!(options.quantization, AnnQuantization::Product { .. })
619 {
620 return Err(MongrelError::Schema(format!(
621 "product-quantizer options supplied for non-Product quantization on index {}",
622 self.name
623 )));
624 }
625 if let AnnQuantization::Product {
628 num_subvectors,
629 bits,
630 } = options.quantization
631 {
632 if num_subvectors == 0 || bits != 8 {
633 return Err(MongrelError::Schema(format!(
634 "invalid product quantization for index {} (num_subvectors > 0, bits == 8)",
635 self.name
636 )));
637 }
638 }
639 if let Some(product) = &options.product {
640 if product.training_samples == 0 || product.rerank_factor > 1024 {
641 return Err(MongrelError::Schema(format!(
642 "invalid product-quantizer training options for index {}",
643 self.name
644 )));
645 }
646 }
647 #[allow(clippy::match_like_matches_macro)]
655 let supported = match (options.algorithm, options.quantization) {
656 (AnnAlgorithm::Hnsw, AnnQuantization::BinarySign) => true,
657 (AnnAlgorithm::Hnsw, AnnQuantization::Dense) => true,
658 (AnnAlgorithm::Hnsw, AnnQuantization::Product { .. }) => true,
662 (AnnAlgorithm::DiskAnn, AnnQuantization::Dense) => true,
664 (AnnAlgorithm::Ivf, AnnQuantization::Dense) => true,
666 _ => false,
667 };
668 if !supported {
669 return Err(MongrelError::Schema(format!(
670 "ANN algorithm {:?} with quantization {:?} is not supported on index {}",
671 options.algorithm, options.quantization, self.name
672 )));
673 }
674 }
675 if let Some(options) = &self.options.minhash {
676 if options.permutations == 0
677 || options.bands == 0
678 || options.permutations % options.bands != 0
679 || options.permutations > 4096
680 || options.bands > 1024
681 {
682 return Err(MongrelError::Schema(format!(
683 "invalid MinHash options for index {}",
684 self.name
685 )));
686 }
687 }
688 if self
689 .options
690 .learned_range
691 .as_ref()
692 .is_some_and(|options| options.epsilon == 0 || options.epsilon > 1_048_576)
693 {
694 return Err(MongrelError::Schema(format!(
695 "invalid learned-range options for index {}",
696 self.name
697 )));
698 }
699 Ok(())
700 }
701}
702
703#[derive(Debug, Clone, Default, Serialize, Deserialize)]
704pub struct Schema {
705 pub schema_id: u64,
706 pub columns: Vec<ColumnDef>,
707 pub indexes: Vec<IndexDef>,
708 #[serde(default)]
713 pub colocation: Vec<Vec<u16>>,
714 #[serde(default)]
719 pub constraints: TableConstraints,
720 #[serde(default)]
723 pub clustered: bool,
724}
725
726impl Schema {
727 pub const MAX_EMBEDDING_DIM: u32 = 65_536;
728
729 pub fn column(&self, name: &str) -> Option<&ColumnDef> {
730 self.columns.iter().find(|c| c.name == name)
731 }
732
733 pub fn primary_key(&self) -> Option<&ColumnDef> {
734 self.columns
735 .iter()
736 .find(|c| c.flags.contains(ColumnFlags::PRIMARY_KEY))
737 }
738
739 pub fn validate_ai(&self) -> Result<()> {
741 for column in &self.columns {
742 let TypeId::Embedding { dim } = column.ty else {
743 if column.embedding_source.is_some() {
744 return Err(MongrelError::Schema(format!(
745 "non-embedding column '{}' cannot define an embedding source",
746 column.name
747 )));
748 }
749 continue;
750 };
751 if dim == 0 || dim > Self::MAX_EMBEDDING_DIM {
752 return Err(MongrelError::Schema(format!(
753 "embedding column '{}' dimension must be between 1 and {}",
754 column.name,
755 Self::MAX_EMBEDDING_DIM
756 )));
757 }
758 match column.embedding_source.as_ref() {
759 None | Some(crate::embedding::EmbeddingSource::SuppliedByApplication) => {}
760 Some(crate::embedding::EmbeddingSource::LocalModel { model_id, .. }) => {
761 if model_id.is_empty() {
762 return Err(MongrelError::Schema(format!(
763 "legacy local embedding column '{}' requires a model identity",
764 column.name
765 )));
766 }
767 }
768 Some(crate::embedding::EmbeddingSource::GeneratedColumn { provider }) => {
769 if provider.is_empty() {
770 return Err(MongrelError::Schema(format!(
771 "legacy generated embedding column '{}' requires a provider identity",
772 column.name
773 )));
774 }
775 }
776 Some(crate::embedding::EmbeddingSource::ConfiguredModel {
777 provider_id,
778 model_id,
779 model_version,
780 }) => {
781 if provider_id.is_empty() || model_id.is_empty() || model_version.is_empty() {
782 return Err(MongrelError::Schema(format!(
783 "embedding column '{}' requires provider, model, and version identities",
784 column.name
785 )));
786 }
787 }
788 Some(crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec }) => {
789 if spec.provider_id.is_empty()
790 || spec.model_id.is_empty()
791 || spec.model_version.is_empty()
792 || spec.source_columns.is_empty()
793 {
794 return Err(MongrelError::Schema(format!(
795 "generated embedding column '{}' has incomplete identity or sources",
796 column.name
797 )));
798 }
799 if spec.dimension != dim {
800 return Err(MongrelError::Schema(format!(
801 "generated embedding column '{}' dimension {} does not match column dimension {}",
802 column.name, spec.dimension, dim
803 )));
804 }
805 let mut sources = std::collections::HashSet::new();
806 for source_id in &spec.source_columns {
807 if *source_id == column.id
808 || !sources.insert(*source_id)
809 || !self.columns.iter().any(|source| source.id == *source_id)
810 {
811 return Err(MongrelError::Schema(format!(
812 "generated embedding column '{}' has invalid source column {}",
813 column.name, source_id
814 )));
815 }
816 }
817 }
818 }
819 }
820 for index in &self.indexes {
821 let column = self
822 .columns
823 .iter()
824 .find(|column| column.id == index.column_id)
825 .ok_or_else(|| {
826 MongrelError::Schema(format!(
827 "index '{}' references unknown column {}",
828 index.name, index.column_id
829 ))
830 })?;
831 let expected = match index.kind {
832 IndexKind::Ann => Some("Embedding"),
833 IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex => Some("Bytes"),
834 _ => None,
835 };
836 if let Some(expected) = expected {
837 let valid = match index.kind {
838 IndexKind::Ann => matches!(column.ty, TypeId::Embedding { .. }),
839 _ => column.ty == TypeId::Bytes,
840 };
841 if !valid {
842 return Err(MongrelError::Schema(format!(
843 "{:?} index '{}' requires a {expected} column",
844 index.kind, index.name
845 )));
846 }
847 if self
848 .indexes
849 .iter()
850 .filter(|other| {
851 other.column_id == index.column_id
852 && matches!(
853 other.kind,
854 IndexKind::Ann
855 | IndexKind::Sparse
856 | IndexKind::MinHash
857 | IndexKind::FmIndex
858 )
859 })
860 .count()
861 > 1
862 {
863 return Err(MongrelError::Schema(format!(
864 "column '{}' may have only one ANN, Sparse, MinHash, or FM representation index",
865 column.name
866 )));
867 }
868 }
869 }
870 Ok(())
871 }
872
873 pub fn validate_values(&self, columns: &[(u16, Value)]) -> Result<()> {
874 self.validate_not_null(columns)?;
875 for (column_id, value) in columns {
876 let Some(column) = self.columns.iter().find(|column| column.id == *column_id) else {
877 return Err(MongrelError::ColumnNotFound(column_id.to_string()));
878 };
879 if !value_matches_type(value, column.ty.clone()) {
880 return Err(MongrelError::InvalidArgument(format!(
881 "column '{}' ({}) value {value:?} does not match type {:?}",
882 column.name, column.id, column.ty
883 )));
884 }
885 let representation = self
886 .indexes
887 .iter()
888 .find(|index| {
889 index.column_id == *column_id
890 && matches!(
891 index.kind,
892 IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex
893 )
894 })
895 .map(|index| index.kind);
896 match representation {
897 Some(IndexKind::Sparse) => match value {
898 Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
899 Value::Bytes(bytes) => {
900 let terms: Vec<(u32, f32)> = bincode::deserialize(bytes).map_err(|_| {
901 MongrelError::InvalidArgument(format!(
902 "sparse column '{}' requires an encoded sparse vector",
903 column.name
904 ))
905 })?;
906 if terms.is_empty() || terms.iter().any(|(_, weight)| !weight.is_finite()) {
907 return Err(MongrelError::InvalidArgument(format!(
908 "sparse column '{}' must be non-empty with finite weights",
909 column.name
910 )));
911 }
912 }
913 _ => {
914 return Err(MongrelError::InvalidArgument(format!(
915 "sparse column '{}' requires bytes or NULL",
916 column.name
917 )));
918 }
919 },
920 Some(IndexKind::MinHash) => match value {
921 Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
922 Value::Bytes(bytes) => {
923 let members: serde_json::Value =
924 serde_json::from_slice(bytes).map_err(|_| {
925 MongrelError::InvalidArgument(format!(
926 "MinHash column '{}' requires a JSON array",
927 column.name
928 ))
929 })?;
930 let serde_json::Value::Array(members) = members else {
931 return Err(MongrelError::InvalidArgument(format!(
932 "MinHash column '{}' requires a JSON array",
933 column.name
934 )));
935 };
936 if members.iter().any(|member| {
937 !matches!(
938 member,
939 serde_json::Value::String(_)
940 | serde_json::Value::Number(_)
941 | serde_json::Value::Bool(_)
942 )
943 }) {
944 return Err(MongrelError::InvalidArgument(format!(
945 "MinHash column '{}' members must be scalar",
946 column.name
947 )));
948 }
949 }
950 _ => {
951 return Err(MongrelError::InvalidArgument(format!(
952 "MinHash column '{}' requires bytes or NULL",
953 column.name
954 )));
955 }
956 },
957 Some(IndexKind::FmIndex) => match value {
958 Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
959 Value::Bytes(_) => {}
960 _ => {
961 return Err(MongrelError::InvalidArgument(format!(
962 "FM text column '{}' requires bytes or NULL",
963 column.name
964 )));
965 }
966 },
967 _ => {}
968 }
969 if let TypeId::Embedding { dim } = &column.ty {
970 let Some(values) = value.as_embedding() else {
971 if matches!(value, Value::Null) {
972 continue;
973 }
974 return Err(MongrelError::InvalidArgument(format!(
975 "embedding column '{}' requires an embedding value",
976 column.name
977 )));
978 };
979 if values.len() != *dim as usize {
980 return Err(MongrelError::InvalidArgument(format!(
981 "embedding column '{}' dimension must be {}, got {}",
982 column.name,
983 dim,
984 values.len()
985 )));
986 }
987 if values.iter().any(|value| !value.is_finite()) {
988 return Err(MongrelError::InvalidArgument(format!(
989 "embedding column '{}' values must be finite",
990 column.name
991 )));
992 }
993 }
994 }
995 Ok(())
996 }
997
998 pub(crate) fn validate_persisted_values(&self, columns: &[(u16, Value)]) -> Result<()> {
1003 let mut resolved = columns.to_vec();
1004 for column in &self.columns {
1005 if column.flags.contains(ColumnFlags::NULLABLE)
1006 || column.flags.contains(ColumnFlags::AUTO_INCREMENT)
1007 {
1008 continue;
1009 }
1010 let position = resolved.iter().position(|(id, _)| *id == column.id);
1011 let missing = position
1012 .map(|index| matches!(resolved[index].1, Value::Null))
1013 .unwrap_or(true);
1014 if !missing {
1015 continue;
1016 }
1017 let Some(default) = &column.default_value else {
1018 continue;
1019 };
1020 let value = match default {
1021 DefaultExpr::Static(value) => value.clone(),
1022 DefaultExpr::Now => match column.ty {
1023 TypeId::Bytes => Value::Bytes(Vec::new()),
1024 TypeId::TimestampNanos | TypeId::Date64 => Value::Int64(0),
1025 _ => unreachable!("validated NOW() default has a temporal/bytes type"),
1026 },
1027 DefaultExpr::Uuid => match column.ty {
1028 TypeId::Uuid => Value::Uuid([0; 16]),
1029 TypeId::Bytes => Value::Bytes(vec![0; 16]),
1030 _ => unreachable!("validated UUID() default has a uuid/bytes type"),
1031 },
1032 };
1033 match position {
1034 Some(index) => resolved[index].1 = value,
1035 None => resolved.push((column.id, value)),
1036 }
1037 }
1038 self.validate_values(&resolved)
1039 }
1040
1041 pub fn validate_not_null(&self, columns: &[(u16, Value)]) -> Result<()> {
1046 let at = |id: u16| columns.iter().find(|(c, _)| *c == id).map(|(_, v)| v);
1049 for col in &self.columns {
1050 if !col.flags.contains(ColumnFlags::NULLABLE) {
1051 if col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
1054 match at(col.id) {
1055 None | Some(Value::Null) => continue,
1056 Some(_) => {}
1057 }
1058 }
1059 match at(col.id) {
1060 None => {
1061 return Err(MongrelError::InvalidArgument(format!(
1062 "column '{}' ({}) is NOT NULL but was omitted",
1063 col.name, col.id
1064 )));
1065 }
1066 Some(Value::Null) => {
1067 return Err(MongrelError::InvalidArgument(format!(
1068 "column '{}' ({}) is NOT NULL but got NULL",
1069 col.name, col.id
1070 )));
1071 }
1072 Some(_) => {}
1073 }
1074 }
1075 if let TypeId::Enum { variants } = &col.ty {
1076 match at(col.id) {
1077 None | Some(Value::Null) => {}
1078 Some(Value::Bytes(value))
1079 if variants
1080 .iter()
1081 .any(|variant| variant.as_bytes() == value.as_slice()) => {}
1082 Some(Value::Bytes(value)) => {
1083 return Err(MongrelError::InvalidArgument(format!(
1084 "column '{}' ({}) enum value {:?} is not one of {:?}",
1085 col.name,
1086 col.id,
1087 String::from_utf8_lossy(value),
1088 variants
1089 )));
1090 }
1091 Some(value) => {
1092 return Err(MongrelError::InvalidArgument(format!(
1093 "column '{}' ({}) enum requires a string/bytes value, got {value:?}",
1094 col.name, col.id
1095 )));
1096 }
1097 }
1098 }
1099 }
1100 Ok(())
1101 }
1102
1103 pub fn validate_auto_increment(&self) -> Result<()> {
1107 const ALLOWED_FLAGS: u32 = ColumnFlags::NULLABLE
1108 | ColumnFlags::PRIMARY_KEY
1109 | ColumnFlags::ENCRYPTED
1110 | ColumnFlags::ENCRYPTED_INDEXABLE
1111 | ColumnFlags::EMBEDDING_BINARY_QUANTIZED
1112 | ColumnFlags::AUTO_INCREMENT;
1113 const FIRST_RESERVED_COLUMN_ID: u16 = 0xFFFC;
1114 let mut ids = std::collections::HashSet::new();
1115 let mut names = std::collections::HashSet::new();
1116 let mut primary_keys = 0_u8;
1117 let mut seen: Option<&ColumnDef> = None;
1118 for col in &self.columns {
1119 if col.id >= FIRST_RESERVED_COLUMN_ID
1120 || col.name.is_empty()
1121 || col.flags.bits() & !ALLOWED_FLAGS != 0
1122 || !ids.insert(col.id)
1123 || !names.insert(col.name.as_str())
1124 {
1125 return Err(MongrelError::Schema(format!(
1126 "column {:?} has a reserved/duplicate identity or unknown flags",
1127 col.name
1128 )));
1129 }
1130 if col.flags.contains(ColumnFlags::PRIMARY_KEY) {
1131 primary_keys = primary_keys.saturating_add(1);
1132 if primary_keys > 1 {
1133 return Err(MongrelError::Schema(
1134 "schema may contain at most one primary key column".into(),
1135 ));
1136 }
1137 }
1138 if !col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
1139 continue;
1140 }
1141 if let Some(prev) = seen {
1142 return Err(MongrelError::Schema(format!(
1143 "AUTO_INCREMENT may be set on at most one column; '{}' and '{}' both carry it",
1144 prev.name, col.name
1145 )));
1146 }
1147 if col.ty != TypeId::Int64 {
1148 return Err(MongrelError::Schema(format!(
1149 "AUTO_INCREMENT column '{}' must be Int64, is {:?}",
1150 col.name, col.ty
1151 )));
1152 }
1153 if !col.flags.contains(ColumnFlags::PRIMARY_KEY) {
1154 return Err(MongrelError::Schema(format!(
1155 "AUTO_INCREMENT column '{}' must also be the primary key",
1156 col.name
1157 )));
1158 }
1159 if col.flags.contains(ColumnFlags::NULLABLE) {
1160 return Err(MongrelError::Schema(format!(
1161 "AUTO_INCREMENT column '{}' must not be nullable",
1162 col.name
1163 )));
1164 }
1165 seen = Some(col);
1166 }
1167 Ok(())
1168 }
1169
1170 pub fn auto_increment_column(&self) -> Option<&ColumnDef> {
1172 self.columns
1173 .iter()
1174 .find(|c| c.flags.contains(ColumnFlags::AUTO_INCREMENT))
1175 }
1176
1177 pub fn validate_defaults(&self) -> Result<()> {
1181 for col in &self.columns {
1182 let Some(expr) = &col.default_value else {
1183 continue;
1184 };
1185 match expr {
1186 DefaultExpr::Static(v) => {
1187 if !value_matches_type(v, col.ty.clone()) {
1188 return Err(MongrelError::Schema(format!(
1189 "DEFAULT value for column '{}' ({:?}) does not match type {:?}",
1190 col.name, v, col.ty
1191 )));
1192 }
1193 }
1194 DefaultExpr::Now => {
1195 if !matches!(
1196 col.ty,
1197 TypeId::Bytes | TypeId::TimestampNanos | TypeId::Date64
1198 ) {
1199 return Err(MongrelError::Schema(format!(
1200 "DEFAULT NOW() on column '{}' requires Bytes/TimestampNanos/Date64, is {:?}",
1201 col.name, col.ty
1202 )));
1203 }
1204 }
1205 DefaultExpr::Uuid => {
1206 if !matches!(col.ty, TypeId::Uuid | TypeId::Bytes) {
1207 return Err(MongrelError::Schema(format!(
1208 "DEFAULT UUID() on column '{}' requires Uuid/Bytes, is {:?}",
1209 col.name, col.ty
1210 )));
1211 }
1212 }
1213 }
1214 }
1215 Ok(())
1216 }
1217}
1218
1219pub(crate) fn value_matches_type(v: &Value, ty: TypeId) -> bool {
1223 matches!(
1224 (v, ty),
1225 (Value::Null, _)
1226 | (Value::Bool(_), TypeId::Bool)
1227 | (
1228 Value::Int64(_),
1229 TypeId::Int8 | TypeId::Int16 | TypeId::Int32 | TypeId::Int64
1230 )
1231 | (Value::Float64(_), TypeId::Float32 | TypeId::Float64)
1232 | (
1233 Value::Bytes(_),
1234 TypeId::Bytes
1235 | TypeId::Json
1236 | TypeId::Uuid
1237 | TypeId::Date64
1238 | TypeId::Time64
1239 | TypeId::Enum { .. }
1240 )
1241 | (
1242 Value::Int64(_),
1243 TypeId::TimestampNanos | TypeId::Date32 | TypeId::Date64 | TypeId::Time64
1244 )
1245 | (Value::Uuid(_), TypeId::Uuid)
1246 | (Value::Decimal(_), TypeId::Decimal128 { .. })
1247 | (Value::Json(_), TypeId::Json)
1248 | (Value::Embedding(_), TypeId::Embedding { .. })
1249 | (Value::GeneratedEmbedding(_), TypeId::Embedding { .. })
1250 | (Value::Interval { .. }, TypeId::Interval)
1251 )
1252}
1253
1254#[cfg(test)]
1255mod tests {
1256 use super::*;
1257
1258 #[test]
1259 fn index_options_preserve_defaults_and_validate_bounds() {
1260 let defaults = IndexDef {
1261 name: "ann".into(),
1262 column_id: 1,
1263 kind: IndexKind::Ann,
1264 predicate: None,
1265 options: IndexOptions::default(),
1266 };
1267 assert!(defaults.validate_options().is_ok());
1268 let json = serde_json::to_string(&defaults).unwrap();
1269 let restored: IndexDef = serde_json::from_str(&json).unwrap();
1270 assert!(restored.options.ann.is_none());
1271 let legacy: IndexDef = serde_json::from_value(serde_json::json!({
1272 "name": "legacy_ann",
1273 "column_id": 1,
1274 "kind": "Ann"
1275 }))
1276 .unwrap();
1277 assert!(legacy.options.ann.is_none());
1278
1279 let invalid = IndexDef {
1280 name: "minhash".into(),
1281 column_id: 2,
1282 kind: IndexKind::MinHash,
1283 predicate: None,
1284 options: IndexOptions {
1285 minhash: Some(MinHashOptions {
1286 permutations: 127,
1287 bands: 32,
1288 }),
1289 ..Default::default()
1290 },
1291 };
1292 assert!(invalid.validate_options().is_err());
1293 }
1294
1295 #[test]
1296 fn flag_composition() {
1297 let f = ColumnFlags::empty()
1298 .with(ColumnFlags::PRIMARY_KEY)
1299 .with(ColumnFlags::ENCRYPTED_INDEXABLE);
1300 assert!(f.contains(ColumnFlags::PRIMARY_KEY));
1301 assert!(f.contains(ColumnFlags::ENCRYPTED_INDEXABLE));
1302 assert!(!f.contains(ColumnFlags::ENCRYPTED));
1303 }
1304
1305 #[test]
1306 fn fixed_size() {
1307 assert_eq!(TypeId::Int64.fixed_size(), Some(8));
1308 assert_eq!(TypeId::Bytes.fixed_size(), None);
1309 assert_eq!(TypeId::Embedding { dim: 768 }.fixed_size(), None);
1310 }
1311
1312 fn col(id: u16, name: &str, ty: TypeId, flags: ColumnFlags) -> ColumnDef {
1313 ColumnDef {
1314 id,
1315 name: name.into(),
1316 ty,
1317 flags,
1318 default_value: None,
1319 embedding_source: None,
1320 }
1321 }
1322
1323 #[test]
1324 fn auto_increment_validation_accepts_int64_pk() {
1325 let s = Schema {
1326 schema_id: 1,
1327 columns: vec![col(
1328 0,
1329 "id",
1330 TypeId::Int64,
1331 ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1332 )],
1333 indexes: vec![],
1334 colocation: vec![],
1335 constraints: Default::default(),
1336 clustered: false,
1337 };
1338 assert!(s.validate_auto_increment().is_ok());
1339 assert_eq!(s.auto_increment_column().unwrap().id, 0);
1340 }
1341
1342 #[test]
1343 fn auto_increment_validation_rejects_non_pk() {
1344 let s = Schema {
1345 schema_id: 1,
1346 columns: vec![
1347 col(
1348 0,
1349 "id",
1350 TypeId::Int64,
1351 ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
1352 ),
1353 col(
1354 1,
1355 "seq",
1356 TypeId::Int64,
1357 ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
1358 ),
1359 ],
1360 indexes: vec![],
1361 colocation: vec![],
1362 constraints: Default::default(),
1363 clustered: false,
1364 };
1365 assert!(s.validate_auto_increment().is_err());
1366 }
1367
1368 #[test]
1369 fn auto_increment_validation_rejects_non_int64() {
1370 let s = Schema {
1371 schema_id: 1,
1372 columns: vec![col(
1373 0,
1374 "id",
1375 TypeId::Bytes,
1376 ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1377 )],
1378 indexes: vec![],
1379 colocation: vec![],
1380 constraints: Default::default(),
1381 clustered: false,
1382 };
1383 assert!(s.validate_auto_increment().is_err());
1384 }
1385
1386 #[test]
1387 fn auto_increment_validation_rejects_two() {
1388 let s = Schema {
1389 schema_id: 1,
1390 columns: vec![
1391 col(
1392 0,
1393 "id",
1394 TypeId::Int64,
1395 ColumnFlags::empty()
1396 .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1397 ),
1398 col(
1399 1,
1400 "id2",
1401 TypeId::Int64,
1402 ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
1403 ),
1404 ],
1405 indexes: vec![],
1406 colocation: vec![],
1407 constraints: Default::default(),
1408 clustered: false,
1409 };
1410 assert!(s.validate_auto_increment().is_err());
1411 }
1412
1413 #[test]
1414 fn auto_increment_exempt_from_not_null_when_omitted() {
1415 let s = Schema {
1416 schema_id: 1,
1417 columns: vec![
1418 col(
1419 0,
1420 "id",
1421 TypeId::Int64,
1422 ColumnFlags::empty()
1423 .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1424 ),
1425 col(1, "name", TypeId::Bytes, ColumnFlags::empty()),
1426 ],
1427 indexes: vec![],
1428 colocation: vec![],
1429 constraints: Default::default(),
1430 clustered: false,
1431 };
1432 let cols = vec![(1u16, Value::Bytes(b"x".to_vec()))];
1434 assert!(s.validate_not_null(&cols).is_ok());
1435 }
1436
1437 #[test]
1438 fn enum_membership_is_enforced_for_nullable_and_required_columns() {
1439 let variants: std::sync::Arc<[String]> =
1440 vec!["user".to_string(), "admin".to_string()].into();
1441 let required = Schema {
1442 columns: vec![col(
1443 1,
1444 "role",
1445 TypeId::Enum {
1446 variants: variants.clone(),
1447 },
1448 ColumnFlags::empty(),
1449 )],
1450 ..Schema::default()
1451 };
1452 assert!(required
1453 .validate_not_null(&[(1, Value::Bytes(b"user".to_vec()))])
1454 .is_ok());
1455 assert!(required
1456 .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1457 .is_err());
1458
1459 let nullable = Schema {
1460 columns: vec![col(
1461 1,
1462 "role",
1463 TypeId::Enum { variants },
1464 ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1465 )],
1466 ..Schema::default()
1467 };
1468 assert!(nullable.validate_not_null(&[(1, Value::Null)]).is_ok());
1469 assert!(nullable
1470 .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1471 .is_err());
1472 }
1473
1474 fn col_with_default(
1475 id: u16,
1476 name: &str,
1477 ty: TypeId,
1478 flags: ColumnFlags,
1479 dv: DefaultExpr,
1480 ) -> ColumnDef {
1481 ColumnDef {
1482 id,
1483 name: name.into(),
1484 ty,
1485 flags,
1486 default_value: Some(dv),
1487 embedding_source: None,
1488 }
1489 }
1490
1491 #[test]
1492 fn validate_defaults_accepts_matching_static() {
1493 let s = Schema {
1494 schema_id: 1,
1495 columns: vec![col_with_default(
1496 0,
1497 "active",
1498 TypeId::Bool,
1499 ColumnFlags::empty(),
1500 DefaultExpr::Static(Value::Bool(true)),
1501 )],
1502 indexes: vec![],
1503 colocation: vec![],
1504 constraints: Default::default(),
1505 clustered: false,
1506 };
1507 assert!(s.validate_defaults().is_ok());
1508 }
1509
1510 #[test]
1511 fn validate_defaults_rejects_mismatched_static() {
1512 let s = Schema {
1513 schema_id: 1,
1514 columns: vec![col_with_default(
1515 0,
1516 "count",
1517 TypeId::Int64,
1518 ColumnFlags::empty(),
1519 DefaultExpr::Static(Value::Bytes(b"oops".to_vec())),
1520 )],
1521 indexes: vec![],
1522 colocation: vec![],
1523 constraints: Default::default(),
1524 clustered: false,
1525 };
1526 assert!(s.validate_defaults().is_err());
1527 }
1528
1529 #[test]
1530 fn validate_defaults_now_requires_temporal_or_bytes() {
1531 let ok = Schema {
1532 schema_id: 1,
1533 columns: vec![col_with_default(
1534 0,
1535 "ts",
1536 TypeId::Bytes,
1537 ColumnFlags::empty(),
1538 DefaultExpr::Now,
1539 )],
1540 indexes: vec![],
1541 colocation: vec![],
1542 constraints: Default::default(),
1543 clustered: false,
1544 };
1545 assert!(ok.validate_defaults().is_ok());
1546
1547 let bad = Schema {
1548 schema_id: 1,
1549 columns: vec![col_with_default(
1550 0,
1551 "ts",
1552 TypeId::Int64,
1553 ColumnFlags::empty(),
1554 DefaultExpr::Now,
1555 )],
1556 indexes: vec![],
1557 colocation: vec![],
1558 constraints: Default::default(),
1559 clustered: false,
1560 };
1561 assert!(bad.validate_defaults().is_err());
1562 }
1563
1564 #[test]
1565 fn validate_defaults_uuid_requires_uuid_or_bytes() {
1566 let ok = Schema {
1567 schema_id: 1,
1568 columns: vec![col_with_default(
1569 0,
1570 "id",
1571 TypeId::Uuid,
1572 ColumnFlags::empty(),
1573 DefaultExpr::Uuid,
1574 )],
1575 indexes: vec![],
1576 colocation: vec![],
1577 constraints: Default::default(),
1578 clustered: false,
1579 };
1580 assert!(ok.validate_defaults().is_ok());
1581
1582 let bad = Schema {
1583 schema_id: 1,
1584 columns: vec![col_with_default(
1585 0,
1586 "id",
1587 TypeId::Bool,
1588 ColumnFlags::empty(),
1589 DefaultExpr::Uuid,
1590 )],
1591 indexes: vec![],
1592 colocation: vec![],
1593 constraints: Default::default(),
1594 clustered: false,
1595 };
1596 assert!(bad.validate_defaults().is_err());
1597 }
1598
1599 #[test]
1600 fn serde_roundtrip_column_def_with_default() {
1601 let c = col_with_default(
1602 0,
1603 "x",
1604 TypeId::Bytes,
1605 ColumnFlags::empty(),
1606 DefaultExpr::Static(Value::Bytes(b"hello".to_vec())),
1607 );
1608 let json = serde_json::to_string(&c).unwrap();
1609 let de: ColumnDef = serde_json::from_str(&json).unwrap();
1610 assert_eq!(c, de);
1611 let old_json = r#"{"id":0,"name":"y","ty":{"kind":"bytes"},"flags":{"bits":0}}"#;
1613 let old: ColumnDef = serde_json::from_str(old_json).unwrap();
1614 assert!(old.default_value.is_none());
1615 }
1616
1617 fn ann_index_def(name: &str, options: AnnOptions) -> IndexDef {
1620 IndexDef {
1621 name: name.into(),
1622 column_id: 1,
1623 kind: IndexKind::Ann,
1624 predicate: None,
1625 options: IndexOptions {
1626 ann: Some(options),
1627 minhash: None,
1628 learned_range: None,
1629 },
1630 }
1631 }
1632
1633 #[test]
1634 fn ann_options_default_is_hnsw_binary_sign() {
1635 let options = AnnOptions::default();
1636 assert_eq!(options.algorithm, AnnAlgorithm::Hnsw);
1637 assert_eq!(options.quantization, AnnQuantization::BinarySign);
1638 assert!(options.diskann.is_none());
1639 assert!(options.ivf.is_none());
1640 assert!(options.product.is_none());
1641 assert!(ann_index_def("d", options).validate_options().is_ok());
1642 }
1643
1644 #[test]
1645 fn ann_options_hnsw_dense_is_supported() {
1646 let options = AnnOptions {
1647 algorithm: AnnAlgorithm::Hnsw,
1648 quantization: AnnQuantization::Dense,
1649 ..AnnOptions::default()
1650 };
1651 assert!(ann_index_def("d", options).validate_options().is_ok());
1652 }
1653
1654 #[test]
1655 fn ann_options_diskann_binary_sign_rejected_as_unsupported() {
1656 let options = AnnOptions {
1659 algorithm: AnnAlgorithm::DiskAnn,
1660 quantization: AnnQuantization::BinarySign,
1661 diskann: Some(DiskAnnOptions::default()),
1662 ..AnnOptions::default()
1663 };
1664 let err = ann_index_def("d", options).validate_options().unwrap_err();
1665 assert!(err.to_string().contains("not supported"));
1666 }
1667
1668 #[test]
1669 fn ann_options_product_with_hnsw_is_supported() {
1670 let options = AnnOptions {
1674 algorithm: AnnAlgorithm::Hnsw,
1675 quantization: AnnQuantization::Product {
1676 num_subvectors: 8,
1677 bits: 8,
1678 },
1679 product: Some(ProductQuantizerOptions::default()),
1680 ..AnnOptions::default()
1681 };
1682 assert!(ann_index_def("d", options).validate_options().is_ok());
1683 }
1684
1685 #[test]
1686 fn ann_options_product_with_diskann_still_rejected() {
1687 let options = AnnOptions {
1689 algorithm: AnnAlgorithm::DiskAnn,
1690 quantization: AnnQuantization::Product {
1691 num_subvectors: 8,
1692 bits: 8,
1693 },
1694 diskann: Some(DiskAnnOptions::default()),
1695 product: Some(ProductQuantizerOptions::default()),
1696 ..AnnOptions::default()
1697 };
1698 let err = ann_index_def("d", options).validate_options().unwrap_err();
1699 assert!(err.to_string().contains("not supported"));
1700 }
1701
1702 #[test]
1703 fn ann_options_diskann_fields_rejected_without_diskann_algorithm() {
1704 let options = AnnOptions {
1706 algorithm: AnnAlgorithm::Hnsw,
1707 quantization: AnnQuantization::Dense,
1708 diskann: Some(DiskAnnOptions::default()),
1709 ..AnnOptions::default()
1710 };
1711 let err = ann_index_def("d", options).validate_options().unwrap_err();
1712 assert!(err.to_string().contains("DiskANN options"));
1713 }
1714
1715 #[test]
1716 fn ann_options_ivf_fields_rejected_without_ivf_algorithm() {
1717 let options = AnnOptions {
1718 algorithm: AnnAlgorithm::Hnsw,
1719 quantization: AnnQuantization::Dense,
1720 ivf: Some(IvfOptions::default()),
1721 ..AnnOptions::default()
1722 };
1723 let err = ann_index_def("d", options).validate_options().unwrap_err();
1724 assert!(err.to_string().contains("IVF options"));
1725 }
1726
1727 #[test]
1728 fn ann_options_product_fields_rejected_without_product_quantization() {
1729 let options = AnnOptions {
1730 algorithm: AnnAlgorithm::Hnsw,
1731 quantization: AnnQuantization::Dense,
1732 product: Some(ProductQuantizerOptions::default()),
1733 ..AnnOptions::default()
1734 };
1735 let err = ann_index_def("d", options).validate_options().unwrap_err();
1736 assert!(err.to_string().contains("product-quantizer options"));
1737 }
1738
1739 #[test]
1740 fn ann_options_diskann_bounds_validated() {
1741 let options = AnnOptions {
1742 algorithm: AnnAlgorithm::DiskAnn,
1743 quantization: AnnQuantization::Dense,
1744 diskann: Some(DiskAnnOptions {
1745 r: 0,
1746 ..DiskAnnOptions::default()
1747 }),
1748 ..AnnOptions::default()
1749 };
1750 let err = ann_index_def("d", options).validate_options().unwrap_err();
1752 assert!(err.to_string().contains("DiskANN options"));
1753 }
1754
1755 #[test]
1756 fn ann_options_ivf_nprobe_exceeding_nlist_rejected() {
1757 let options = AnnOptions {
1758 algorithm: AnnAlgorithm::Ivf,
1759 quantization: AnnQuantization::Dense,
1760 ivf: Some(IvfOptions {
1761 nlist: 16,
1762 nprobe: 32,
1763 }),
1764 ..AnnOptions::default()
1765 };
1766 let err = ann_index_def("d", options).validate_options().unwrap_err();
1767 assert!(err.to_string().contains("IVF options"));
1768 }
1769
1770 #[test]
1771 fn ann_options_product_bits_other_than_eight_rejected() {
1772 let options = AnnOptions {
1773 algorithm: AnnAlgorithm::Hnsw,
1774 quantization: AnnQuantization::Product {
1775 num_subvectors: 8,
1776 bits: 4,
1777 },
1778 ..AnnOptions::default()
1779 };
1780 let err = ann_index_def("d", options).validate_options().unwrap_err();
1781 assert!(err.to_string().contains("product quantization"));
1782 }
1783
1784 #[test]
1785 fn ann_options_round_trip_through_serde() {
1786 let options = AnnOptions {
1787 algorithm: AnnAlgorithm::DiskAnn,
1788 quantization: AnnQuantization::Dense,
1789 m: 24,
1790 ef_construction: 96,
1791 ef_search: 48,
1792 diskann: Some(DiskAnnOptions {
1793 r: 96,
1794 l: 200,
1795 beam_width: 4,
1796 alpha: 115,
1797 }),
1798 ivf: None,
1799 product: None,
1800 };
1801 let json = serde_json::to_string(&options).unwrap();
1802 let de: AnnOptions = serde_json::from_str(&json).unwrap();
1803 assert_eq!(de, options);
1804 let minimal = r#"{"m":16,"ef_construction":64,"ef_search":64,"quantization":"binary_sign","algorithm":"hnsw"}"#;
1806 let legacy: AnnOptions = serde_json::from_str(minimal).unwrap();
1807 assert_eq!(legacy.algorithm, AnnAlgorithm::Hnsw);
1808 assert!(legacy.diskann.is_none());
1809 }
1810}