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}
297
298impl Default for AnnOptions {
299 fn default() -> Self {
300 Self {
301 m: default_ann_m(),
302 ef_construction: default_ann_ef_construction(),
303 ef_search: default_ann_ef_search(),
304 quantization: AnnQuantization::BinarySign,
305 }
306 }
307}
308
309#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
310#[serde(rename_all = "snake_case")]
311pub enum AnnQuantization {
312 #[default]
313 BinarySign,
314 Dense,
316}
317
318const fn default_ann_m() -> usize {
319 16
320}
321const fn default_ann_ef_construction() -> usize {
322 64
323}
324const fn default_ann_ef_search() -> usize {
325 64
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
329pub struct MinHashOptions {
330 #[serde(default = "default_minhash_permutations")]
331 pub permutations: usize,
332 #[serde(default = "default_minhash_bands")]
333 pub bands: usize,
334}
335
336impl Default for MinHashOptions {
337 fn default() -> Self {
338 Self {
339 permutations: default_minhash_permutations(),
340 bands: default_minhash_bands(),
341 }
342 }
343}
344
345const fn default_minhash_permutations() -> usize {
346 128
347}
348const fn default_minhash_bands() -> usize {
349 32
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353pub struct LearnedRangeOptions {
354 #[serde(default = "default_learned_range_epsilon")]
355 pub epsilon: usize,
356}
357
358impl Default for LearnedRangeOptions {
359 fn default() -> Self {
360 Self {
361 epsilon: default_learned_range_epsilon(),
362 }
363 }
364}
365
366const fn default_learned_range_epsilon() -> usize {
367 16
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
371pub struct IndexDef {
372 pub name: String,
373 pub column_id: u16,
374 pub kind: IndexKind,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub predicate: Option<String>,
380 #[serde(default)]
381 pub options: IndexOptions,
382}
383
384impl IndexDef {
385 pub fn validate_options(&self) -> Result<()> {
386 if self.options.ann.is_some() && self.kind != IndexKind::Ann
387 || self.options.minhash.is_some() && self.kind != IndexKind::MinHash
388 || self.options.learned_range.is_some() && self.kind != IndexKind::LearnedRange
389 {
390 return Err(MongrelError::Schema(format!(
391 "index {} has options for a different index kind",
392 self.name
393 )));
394 }
395 if let Some(options) = &self.options.ann {
396 if options.m == 0
397 || options.ef_construction < options.m
398 || options.ef_search == 0
399 || options.m > 256
400 || options.ef_construction > 65_536
401 || options.ef_search > 65_536
402 {
403 return Err(MongrelError::Schema(format!(
404 "invalid ANN options for index {}",
405 self.name
406 )));
407 }
408 }
409 if let Some(options) = &self.options.minhash {
410 if options.permutations == 0
411 || options.bands == 0
412 || options.permutations % options.bands != 0
413 || options.permutations > 4096
414 || options.bands > 1024
415 {
416 return Err(MongrelError::Schema(format!(
417 "invalid MinHash options for index {}",
418 self.name
419 )));
420 }
421 }
422 if self
423 .options
424 .learned_range
425 .as_ref()
426 .is_some_and(|options| options.epsilon == 0 || options.epsilon > 1_048_576)
427 {
428 return Err(MongrelError::Schema(format!(
429 "invalid learned-range options for index {}",
430 self.name
431 )));
432 }
433 Ok(())
434 }
435}
436
437#[derive(Debug, Clone, Default, Serialize, Deserialize)]
438pub struct Schema {
439 pub schema_id: u64,
440 pub columns: Vec<ColumnDef>,
441 pub indexes: Vec<IndexDef>,
442 #[serde(default)]
447 pub colocation: Vec<Vec<u16>>,
448 #[serde(default)]
453 pub constraints: TableConstraints,
454 #[serde(default)]
457 pub clustered: bool,
458}
459
460impl Schema {
461 pub const MAX_EMBEDDING_DIM: u32 = 65_536;
462
463 pub fn column(&self, name: &str) -> Option<&ColumnDef> {
464 self.columns.iter().find(|c| c.name == name)
465 }
466
467 pub fn primary_key(&self) -> Option<&ColumnDef> {
468 self.columns
469 .iter()
470 .find(|c| c.flags.contains(ColumnFlags::PRIMARY_KEY))
471 }
472
473 pub fn validate_ai(&self) -> Result<()> {
475 for column in &self.columns {
476 let TypeId::Embedding { dim } = column.ty else {
477 if column.embedding_source.is_some() {
478 return Err(MongrelError::Schema(format!(
479 "non-embedding column '{}' cannot define an embedding source",
480 column.name
481 )));
482 }
483 continue;
484 };
485 if dim == 0 || dim > Self::MAX_EMBEDDING_DIM {
486 return Err(MongrelError::Schema(format!(
487 "embedding column '{}' dimension must be between 1 and {}",
488 column.name,
489 Self::MAX_EMBEDDING_DIM
490 )));
491 }
492 match column.embedding_source.as_ref() {
493 None | Some(crate::embedding::EmbeddingSource::SuppliedByApplication) => {}
494 Some(crate::embedding::EmbeddingSource::LocalModel { model_id, .. }) => {
495 if model_id.is_empty() {
496 return Err(MongrelError::Schema(format!(
497 "legacy local embedding column '{}' requires a model identity",
498 column.name
499 )));
500 }
501 }
502 Some(crate::embedding::EmbeddingSource::GeneratedColumn { provider }) => {
503 if provider.is_empty() {
504 return Err(MongrelError::Schema(format!(
505 "legacy generated embedding column '{}' requires a provider identity",
506 column.name
507 )));
508 }
509 }
510 Some(crate::embedding::EmbeddingSource::ConfiguredModel {
511 provider_id,
512 model_id,
513 model_version,
514 }) => {
515 if provider_id.is_empty() || model_id.is_empty() || model_version.is_empty() {
516 return Err(MongrelError::Schema(format!(
517 "embedding column '{}' requires provider, model, and version identities",
518 column.name
519 )));
520 }
521 }
522 Some(crate::embedding::EmbeddingSource::GeneratedColumnSpec { spec }) => {
523 if spec.provider_id.is_empty()
524 || spec.model_id.is_empty()
525 || spec.model_version.is_empty()
526 || spec.source_columns.is_empty()
527 {
528 return Err(MongrelError::Schema(format!(
529 "generated embedding column '{}' has incomplete identity or sources",
530 column.name
531 )));
532 }
533 if spec.dimension != dim {
534 return Err(MongrelError::Schema(format!(
535 "generated embedding column '{}' dimension {} does not match column dimension {}",
536 column.name, spec.dimension, dim
537 )));
538 }
539 let mut sources = std::collections::HashSet::new();
540 for source_id in &spec.source_columns {
541 if *source_id == column.id
542 || !sources.insert(*source_id)
543 || !self.columns.iter().any(|source| source.id == *source_id)
544 {
545 return Err(MongrelError::Schema(format!(
546 "generated embedding column '{}' has invalid source column {}",
547 column.name, source_id
548 )));
549 }
550 }
551 }
552 }
553 }
554 for index in &self.indexes {
555 let column = self
556 .columns
557 .iter()
558 .find(|column| column.id == index.column_id)
559 .ok_or_else(|| {
560 MongrelError::Schema(format!(
561 "index '{}' references unknown column {}",
562 index.name, index.column_id
563 ))
564 })?;
565 let expected = match index.kind {
566 IndexKind::Ann => Some("Embedding"),
567 IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex => Some("Bytes"),
568 _ => None,
569 };
570 if let Some(expected) = expected {
571 let valid = match index.kind {
572 IndexKind::Ann => matches!(column.ty, TypeId::Embedding { .. }),
573 _ => column.ty == TypeId::Bytes,
574 };
575 if !valid {
576 return Err(MongrelError::Schema(format!(
577 "{:?} index '{}' requires a {expected} column",
578 index.kind, index.name
579 )));
580 }
581 if self
582 .indexes
583 .iter()
584 .filter(|other| {
585 other.column_id == index.column_id
586 && matches!(
587 other.kind,
588 IndexKind::Ann
589 | IndexKind::Sparse
590 | IndexKind::MinHash
591 | IndexKind::FmIndex
592 )
593 })
594 .count()
595 > 1
596 {
597 return Err(MongrelError::Schema(format!(
598 "column '{}' may have only one ANN, Sparse, MinHash, or FM representation index",
599 column.name
600 )));
601 }
602 }
603 }
604 Ok(())
605 }
606
607 pub fn validate_values(&self, columns: &[(u16, Value)]) -> Result<()> {
608 self.validate_not_null(columns)?;
609 for (column_id, value) in columns {
610 let Some(column) = self.columns.iter().find(|column| column.id == *column_id) else {
611 return Err(MongrelError::ColumnNotFound(column_id.to_string()));
612 };
613 if !value_matches_type(value, column.ty.clone()) {
614 return Err(MongrelError::InvalidArgument(format!(
615 "column '{}' ({}) value {value:?} does not match type {:?}",
616 column.name, column.id, column.ty
617 )));
618 }
619 let representation = self
620 .indexes
621 .iter()
622 .find(|index| {
623 index.column_id == *column_id
624 && matches!(
625 index.kind,
626 IndexKind::Sparse | IndexKind::MinHash | IndexKind::FmIndex
627 )
628 })
629 .map(|index| index.kind);
630 match representation {
631 Some(IndexKind::Sparse) => match value {
632 Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
633 Value::Bytes(bytes) => {
634 let terms: Vec<(u32, f32)> = bincode::deserialize(bytes).map_err(|_| {
635 MongrelError::InvalidArgument(format!(
636 "sparse column '{}' requires an encoded sparse vector",
637 column.name
638 ))
639 })?;
640 if terms.is_empty() || terms.iter().any(|(_, weight)| !weight.is_finite()) {
641 return Err(MongrelError::InvalidArgument(format!(
642 "sparse column '{}' must be non-empty with finite weights",
643 column.name
644 )));
645 }
646 }
647 _ => {
648 return Err(MongrelError::InvalidArgument(format!(
649 "sparse column '{}' requires bytes or NULL",
650 column.name
651 )));
652 }
653 },
654 Some(IndexKind::MinHash) => match value {
655 Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
656 Value::Bytes(bytes) => {
657 let members: serde_json::Value =
658 serde_json::from_slice(bytes).map_err(|_| {
659 MongrelError::InvalidArgument(format!(
660 "MinHash column '{}' requires a JSON array",
661 column.name
662 ))
663 })?;
664 let serde_json::Value::Array(members) = members else {
665 return Err(MongrelError::InvalidArgument(format!(
666 "MinHash column '{}' requires a JSON array",
667 column.name
668 )));
669 };
670 if members.iter().any(|member| {
671 !matches!(
672 member,
673 serde_json::Value::String(_)
674 | serde_json::Value::Number(_)
675 | serde_json::Value::Bool(_)
676 )
677 }) {
678 return Err(MongrelError::InvalidArgument(format!(
679 "MinHash column '{}' members must be scalar",
680 column.name
681 )));
682 }
683 }
684 _ => {
685 return Err(MongrelError::InvalidArgument(format!(
686 "MinHash column '{}' requires bytes or NULL",
687 column.name
688 )));
689 }
690 },
691 Some(IndexKind::FmIndex) => match value {
692 Value::Null if column.flags.contains(ColumnFlags::NULLABLE) => {}
693 Value::Bytes(_) => {}
694 _ => {
695 return Err(MongrelError::InvalidArgument(format!(
696 "FM text column '{}' requires bytes or NULL",
697 column.name
698 )));
699 }
700 },
701 _ => {}
702 }
703 if let TypeId::Embedding { dim } = &column.ty {
704 let Some(values) = value.as_embedding() else {
705 if matches!(value, Value::Null) {
706 continue;
707 }
708 return Err(MongrelError::InvalidArgument(format!(
709 "embedding column '{}' requires an embedding value",
710 column.name
711 )));
712 };
713 if values.len() != *dim as usize {
714 return Err(MongrelError::InvalidArgument(format!(
715 "embedding column '{}' dimension must be {}, got {}",
716 column.name,
717 dim,
718 values.len()
719 )));
720 }
721 if values.iter().any(|value| !value.is_finite()) {
722 return Err(MongrelError::InvalidArgument(format!(
723 "embedding column '{}' values must be finite",
724 column.name
725 )));
726 }
727 }
728 }
729 Ok(())
730 }
731
732 pub(crate) fn validate_persisted_values(&self, columns: &[(u16, Value)]) -> Result<()> {
737 let mut resolved = columns.to_vec();
738 for column in &self.columns {
739 if column.flags.contains(ColumnFlags::NULLABLE)
740 || column.flags.contains(ColumnFlags::AUTO_INCREMENT)
741 {
742 continue;
743 }
744 let position = resolved.iter().position(|(id, _)| *id == column.id);
745 let missing = position
746 .map(|index| matches!(resolved[index].1, Value::Null))
747 .unwrap_or(true);
748 if !missing {
749 continue;
750 }
751 let Some(default) = &column.default_value else {
752 continue;
753 };
754 let value = match default {
755 DefaultExpr::Static(value) => value.clone(),
756 DefaultExpr::Now => match column.ty {
757 TypeId::Bytes => Value::Bytes(Vec::new()),
758 TypeId::TimestampNanos | TypeId::Date64 => Value::Int64(0),
759 _ => unreachable!("validated NOW() default has a temporal/bytes type"),
760 },
761 DefaultExpr::Uuid => match column.ty {
762 TypeId::Uuid => Value::Uuid([0; 16]),
763 TypeId::Bytes => Value::Bytes(vec![0; 16]),
764 _ => unreachable!("validated UUID() default has a uuid/bytes type"),
765 },
766 };
767 match position {
768 Some(index) => resolved[index].1 = value,
769 None => resolved.push((column.id, value)),
770 }
771 }
772 self.validate_values(&resolved)
773 }
774
775 pub fn validate_not_null(&self, columns: &[(u16, Value)]) -> Result<()> {
780 let at = |id: u16| columns.iter().find(|(c, _)| *c == id).map(|(_, v)| v);
783 for col in &self.columns {
784 if !col.flags.contains(ColumnFlags::NULLABLE) {
785 if col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
788 match at(col.id) {
789 None | Some(Value::Null) => continue,
790 Some(_) => {}
791 }
792 }
793 match at(col.id) {
794 None => {
795 return Err(MongrelError::InvalidArgument(format!(
796 "column '{}' ({}) is NOT NULL but was omitted",
797 col.name, col.id
798 )));
799 }
800 Some(Value::Null) => {
801 return Err(MongrelError::InvalidArgument(format!(
802 "column '{}' ({}) is NOT NULL but got NULL",
803 col.name, col.id
804 )));
805 }
806 Some(_) => {}
807 }
808 }
809 if let TypeId::Enum { variants } = &col.ty {
810 match at(col.id) {
811 None | Some(Value::Null) => {}
812 Some(Value::Bytes(value))
813 if variants
814 .iter()
815 .any(|variant| variant.as_bytes() == value.as_slice()) => {}
816 Some(Value::Bytes(value)) => {
817 return Err(MongrelError::InvalidArgument(format!(
818 "column '{}' ({}) enum value {:?} is not one of {:?}",
819 col.name,
820 col.id,
821 String::from_utf8_lossy(value),
822 variants
823 )));
824 }
825 Some(value) => {
826 return Err(MongrelError::InvalidArgument(format!(
827 "column '{}' ({}) enum requires a string/bytes value, got {value:?}",
828 col.name, col.id
829 )));
830 }
831 }
832 }
833 }
834 Ok(())
835 }
836
837 pub fn validate_auto_increment(&self) -> Result<()> {
841 const ALLOWED_FLAGS: u32 = ColumnFlags::NULLABLE
842 | ColumnFlags::PRIMARY_KEY
843 | ColumnFlags::ENCRYPTED
844 | ColumnFlags::ENCRYPTED_INDEXABLE
845 | ColumnFlags::EMBEDDING_BINARY_QUANTIZED
846 | ColumnFlags::AUTO_INCREMENT;
847 const FIRST_RESERVED_COLUMN_ID: u16 = 0xFFFC;
848 let mut ids = std::collections::HashSet::new();
849 let mut names = std::collections::HashSet::new();
850 let mut primary_keys = 0_u8;
851 let mut seen: Option<&ColumnDef> = None;
852 for col in &self.columns {
853 if col.id >= FIRST_RESERVED_COLUMN_ID
854 || col.name.is_empty()
855 || col.flags.bits() & !ALLOWED_FLAGS != 0
856 || !ids.insert(col.id)
857 || !names.insert(col.name.as_str())
858 {
859 return Err(MongrelError::Schema(format!(
860 "column {:?} has a reserved/duplicate identity or unknown flags",
861 col.name
862 )));
863 }
864 if col.flags.contains(ColumnFlags::PRIMARY_KEY) {
865 primary_keys = primary_keys.saturating_add(1);
866 if primary_keys > 1 {
867 return Err(MongrelError::Schema(
868 "schema may contain at most one primary key column".into(),
869 ));
870 }
871 }
872 if !col.flags.contains(ColumnFlags::AUTO_INCREMENT) {
873 continue;
874 }
875 if let Some(prev) = seen {
876 return Err(MongrelError::Schema(format!(
877 "AUTO_INCREMENT may be set on at most one column; '{}' and '{}' both carry it",
878 prev.name, col.name
879 )));
880 }
881 if col.ty != TypeId::Int64 {
882 return Err(MongrelError::Schema(format!(
883 "AUTO_INCREMENT column '{}' must be Int64, is {:?}",
884 col.name, col.ty
885 )));
886 }
887 if !col.flags.contains(ColumnFlags::PRIMARY_KEY) {
888 return Err(MongrelError::Schema(format!(
889 "AUTO_INCREMENT column '{}' must also be the primary key",
890 col.name
891 )));
892 }
893 if col.flags.contains(ColumnFlags::NULLABLE) {
894 return Err(MongrelError::Schema(format!(
895 "AUTO_INCREMENT column '{}' must not be nullable",
896 col.name
897 )));
898 }
899 seen = Some(col);
900 }
901 Ok(())
902 }
903
904 pub fn auto_increment_column(&self) -> Option<&ColumnDef> {
906 self.columns
907 .iter()
908 .find(|c| c.flags.contains(ColumnFlags::AUTO_INCREMENT))
909 }
910
911 pub fn validate_defaults(&self) -> Result<()> {
915 for col in &self.columns {
916 let Some(expr) = &col.default_value else {
917 continue;
918 };
919 match expr {
920 DefaultExpr::Static(v) => {
921 if !value_matches_type(v, col.ty.clone()) {
922 return Err(MongrelError::Schema(format!(
923 "DEFAULT value for column '{}' ({:?}) does not match type {:?}",
924 col.name, v, col.ty
925 )));
926 }
927 }
928 DefaultExpr::Now => {
929 if !matches!(
930 col.ty,
931 TypeId::Bytes | TypeId::TimestampNanos | TypeId::Date64
932 ) {
933 return Err(MongrelError::Schema(format!(
934 "DEFAULT NOW() on column '{}' requires Bytes/TimestampNanos/Date64, is {:?}",
935 col.name, col.ty
936 )));
937 }
938 }
939 DefaultExpr::Uuid => {
940 if !matches!(col.ty, TypeId::Uuid | TypeId::Bytes) {
941 return Err(MongrelError::Schema(format!(
942 "DEFAULT UUID() on column '{}' requires Uuid/Bytes, is {:?}",
943 col.name, col.ty
944 )));
945 }
946 }
947 }
948 }
949 Ok(())
950 }
951}
952
953pub(crate) fn value_matches_type(v: &Value, ty: TypeId) -> bool {
957 matches!(
958 (v, ty),
959 (Value::Null, _)
960 | (Value::Bool(_), TypeId::Bool)
961 | (
962 Value::Int64(_),
963 TypeId::Int8 | TypeId::Int16 | TypeId::Int32 | TypeId::Int64
964 )
965 | (Value::Float64(_), TypeId::Float32 | TypeId::Float64)
966 | (
967 Value::Bytes(_),
968 TypeId::Bytes
969 | TypeId::Json
970 | TypeId::Uuid
971 | TypeId::Date64
972 | TypeId::Time64
973 | TypeId::Enum { .. }
974 )
975 | (
976 Value::Int64(_),
977 TypeId::TimestampNanos | TypeId::Date32 | TypeId::Date64 | TypeId::Time64
978 )
979 | (Value::Uuid(_), TypeId::Uuid)
980 | (Value::Decimal(_), TypeId::Decimal128 { .. })
981 | (Value::Json(_), TypeId::Json)
982 | (Value::Embedding(_), TypeId::Embedding { .. })
983 | (Value::GeneratedEmbedding(_), TypeId::Embedding { .. })
984 | (Value::Interval { .. }, TypeId::Interval)
985 )
986}
987
988#[cfg(test)]
989mod tests {
990 use super::*;
991
992 #[test]
993 fn index_options_preserve_defaults_and_validate_bounds() {
994 let defaults = IndexDef {
995 name: "ann".into(),
996 column_id: 1,
997 kind: IndexKind::Ann,
998 predicate: None,
999 options: IndexOptions::default(),
1000 };
1001 assert!(defaults.validate_options().is_ok());
1002 let json = serde_json::to_string(&defaults).unwrap();
1003 let restored: IndexDef = serde_json::from_str(&json).unwrap();
1004 assert!(restored.options.ann.is_none());
1005 let legacy: IndexDef = serde_json::from_value(serde_json::json!({
1006 "name": "legacy_ann",
1007 "column_id": 1,
1008 "kind": "Ann"
1009 }))
1010 .unwrap();
1011 assert!(legacy.options.ann.is_none());
1012
1013 let invalid = IndexDef {
1014 name: "minhash".into(),
1015 column_id: 2,
1016 kind: IndexKind::MinHash,
1017 predicate: None,
1018 options: IndexOptions {
1019 minhash: Some(MinHashOptions {
1020 permutations: 127,
1021 bands: 32,
1022 }),
1023 ..Default::default()
1024 },
1025 };
1026 assert!(invalid.validate_options().is_err());
1027 }
1028
1029 #[test]
1030 fn flag_composition() {
1031 let f = ColumnFlags::empty()
1032 .with(ColumnFlags::PRIMARY_KEY)
1033 .with(ColumnFlags::ENCRYPTED_INDEXABLE);
1034 assert!(f.contains(ColumnFlags::PRIMARY_KEY));
1035 assert!(f.contains(ColumnFlags::ENCRYPTED_INDEXABLE));
1036 assert!(!f.contains(ColumnFlags::ENCRYPTED));
1037 }
1038
1039 #[test]
1040 fn fixed_size() {
1041 assert_eq!(TypeId::Int64.fixed_size(), Some(8));
1042 assert_eq!(TypeId::Bytes.fixed_size(), None);
1043 assert_eq!(TypeId::Embedding { dim: 768 }.fixed_size(), None);
1044 }
1045
1046 fn col(id: u16, name: &str, ty: TypeId, flags: ColumnFlags) -> ColumnDef {
1047 ColumnDef {
1048 id,
1049 name: name.into(),
1050 ty,
1051 flags,
1052 default_value: None,
1053 embedding_source: None,
1054 }
1055 }
1056
1057 #[test]
1058 fn auto_increment_validation_accepts_int64_pk() {
1059 let s = Schema {
1060 schema_id: 1,
1061 columns: vec![col(
1062 0,
1063 "id",
1064 TypeId::Int64,
1065 ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1066 )],
1067 indexes: vec![],
1068 colocation: vec![],
1069 constraints: Default::default(),
1070 clustered: false,
1071 };
1072 assert!(s.validate_auto_increment().is_ok());
1073 assert_eq!(s.auto_increment_column().unwrap().id, 0);
1074 }
1075
1076 #[test]
1077 fn auto_increment_validation_rejects_non_pk() {
1078 let s = Schema {
1079 schema_id: 1,
1080 columns: vec![
1081 col(
1082 0,
1083 "id",
1084 TypeId::Int64,
1085 ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
1086 ),
1087 col(
1088 1,
1089 "seq",
1090 TypeId::Int64,
1091 ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
1092 ),
1093 ],
1094 indexes: vec![],
1095 colocation: vec![],
1096 constraints: Default::default(),
1097 clustered: false,
1098 };
1099 assert!(s.validate_auto_increment().is_err());
1100 }
1101
1102 #[test]
1103 fn auto_increment_validation_rejects_non_int64() {
1104 let s = Schema {
1105 schema_id: 1,
1106 columns: vec![col(
1107 0,
1108 "id",
1109 TypeId::Bytes,
1110 ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1111 )],
1112 indexes: vec![],
1113 colocation: vec![],
1114 constraints: Default::default(),
1115 clustered: false,
1116 };
1117 assert!(s.validate_auto_increment().is_err());
1118 }
1119
1120 #[test]
1121 fn auto_increment_validation_rejects_two() {
1122 let s = Schema {
1123 schema_id: 1,
1124 columns: vec![
1125 col(
1126 0,
1127 "id",
1128 TypeId::Int64,
1129 ColumnFlags::empty()
1130 .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1131 ),
1132 col(
1133 1,
1134 "id2",
1135 TypeId::Int64,
1136 ColumnFlags::empty().with(ColumnFlags::AUTO_INCREMENT),
1137 ),
1138 ],
1139 indexes: vec![],
1140 colocation: vec![],
1141 constraints: Default::default(),
1142 clustered: false,
1143 };
1144 assert!(s.validate_auto_increment().is_err());
1145 }
1146
1147 #[test]
1148 fn auto_increment_exempt_from_not_null_when_omitted() {
1149 let s = Schema {
1150 schema_id: 1,
1151 columns: vec![
1152 col(
1153 0,
1154 "id",
1155 TypeId::Int64,
1156 ColumnFlags::empty()
1157 .with(ColumnFlags::PRIMARY_KEY | ColumnFlags::AUTO_INCREMENT),
1158 ),
1159 col(1, "name", TypeId::Bytes, ColumnFlags::empty()),
1160 ],
1161 indexes: vec![],
1162 colocation: vec![],
1163 constraints: Default::default(),
1164 clustered: false,
1165 };
1166 let cols = vec![(1u16, Value::Bytes(b"x".to_vec()))];
1168 assert!(s.validate_not_null(&cols).is_ok());
1169 }
1170
1171 #[test]
1172 fn enum_membership_is_enforced_for_nullable_and_required_columns() {
1173 let variants: std::sync::Arc<[String]> =
1174 vec!["user".to_string(), "admin".to_string()].into();
1175 let required = Schema {
1176 columns: vec![col(
1177 1,
1178 "role",
1179 TypeId::Enum {
1180 variants: variants.clone(),
1181 },
1182 ColumnFlags::empty(),
1183 )],
1184 ..Schema::default()
1185 };
1186 assert!(required
1187 .validate_not_null(&[(1, Value::Bytes(b"user".to_vec()))])
1188 .is_ok());
1189 assert!(required
1190 .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1191 .is_err());
1192
1193 let nullable = Schema {
1194 columns: vec![col(
1195 1,
1196 "role",
1197 TypeId::Enum { variants },
1198 ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1199 )],
1200 ..Schema::default()
1201 };
1202 assert!(nullable.validate_not_null(&[(1, Value::Null)]).is_ok());
1203 assert!(nullable
1204 .validate_not_null(&[(1, Value::Bytes(b"owner".to_vec()))])
1205 .is_err());
1206 }
1207
1208 fn col_with_default(
1209 id: u16,
1210 name: &str,
1211 ty: TypeId,
1212 flags: ColumnFlags,
1213 dv: DefaultExpr,
1214 ) -> ColumnDef {
1215 ColumnDef {
1216 id,
1217 name: name.into(),
1218 ty,
1219 flags,
1220 default_value: Some(dv),
1221 embedding_source: None,
1222 }
1223 }
1224
1225 #[test]
1226 fn validate_defaults_accepts_matching_static() {
1227 let s = Schema {
1228 schema_id: 1,
1229 columns: vec![col_with_default(
1230 0,
1231 "active",
1232 TypeId::Bool,
1233 ColumnFlags::empty(),
1234 DefaultExpr::Static(Value::Bool(true)),
1235 )],
1236 indexes: vec![],
1237 colocation: vec![],
1238 constraints: Default::default(),
1239 clustered: false,
1240 };
1241 assert!(s.validate_defaults().is_ok());
1242 }
1243
1244 #[test]
1245 fn validate_defaults_rejects_mismatched_static() {
1246 let s = Schema {
1247 schema_id: 1,
1248 columns: vec![col_with_default(
1249 0,
1250 "count",
1251 TypeId::Int64,
1252 ColumnFlags::empty(),
1253 DefaultExpr::Static(Value::Bytes(b"oops".to_vec())),
1254 )],
1255 indexes: vec![],
1256 colocation: vec![],
1257 constraints: Default::default(),
1258 clustered: false,
1259 };
1260 assert!(s.validate_defaults().is_err());
1261 }
1262
1263 #[test]
1264 fn validate_defaults_now_requires_temporal_or_bytes() {
1265 let ok = Schema {
1266 schema_id: 1,
1267 columns: vec![col_with_default(
1268 0,
1269 "ts",
1270 TypeId::Bytes,
1271 ColumnFlags::empty(),
1272 DefaultExpr::Now,
1273 )],
1274 indexes: vec![],
1275 colocation: vec![],
1276 constraints: Default::default(),
1277 clustered: false,
1278 };
1279 assert!(ok.validate_defaults().is_ok());
1280
1281 let bad = Schema {
1282 schema_id: 1,
1283 columns: vec![col_with_default(
1284 0,
1285 "ts",
1286 TypeId::Int64,
1287 ColumnFlags::empty(),
1288 DefaultExpr::Now,
1289 )],
1290 indexes: vec![],
1291 colocation: vec![],
1292 constraints: Default::default(),
1293 clustered: false,
1294 };
1295 assert!(bad.validate_defaults().is_err());
1296 }
1297
1298 #[test]
1299 fn validate_defaults_uuid_requires_uuid_or_bytes() {
1300 let ok = Schema {
1301 schema_id: 1,
1302 columns: vec![col_with_default(
1303 0,
1304 "id",
1305 TypeId::Uuid,
1306 ColumnFlags::empty(),
1307 DefaultExpr::Uuid,
1308 )],
1309 indexes: vec![],
1310 colocation: vec![],
1311 constraints: Default::default(),
1312 clustered: false,
1313 };
1314 assert!(ok.validate_defaults().is_ok());
1315
1316 let bad = Schema {
1317 schema_id: 1,
1318 columns: vec![col_with_default(
1319 0,
1320 "id",
1321 TypeId::Bool,
1322 ColumnFlags::empty(),
1323 DefaultExpr::Uuid,
1324 )],
1325 indexes: vec![],
1326 colocation: vec![],
1327 constraints: Default::default(),
1328 clustered: false,
1329 };
1330 assert!(bad.validate_defaults().is_err());
1331 }
1332
1333 #[test]
1334 fn serde_roundtrip_column_def_with_default() {
1335 let c = col_with_default(
1336 0,
1337 "x",
1338 TypeId::Bytes,
1339 ColumnFlags::empty(),
1340 DefaultExpr::Static(Value::Bytes(b"hello".to_vec())),
1341 );
1342 let json = serde_json::to_string(&c).unwrap();
1343 let de: ColumnDef = serde_json::from_str(&json).unwrap();
1344 assert_eq!(c, de);
1345 let old_json = r#"{"id":0,"name":"y","ty":{"kind":"bytes"},"flags":{"bits":0}}"#;
1347 let old: ColumnDef = serde_json::from_str(old_json).unwrap();
1348 assert!(old.default_value.is_none());
1349 }
1350}