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