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