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