1use std::collections::HashMap;
35use std::fmt;
36use std::sync::Arc;
37
38use dashmap::DashMap;
39use serde::{Deserialize, Serialize};
40
41use crate::error::SchemaError;
42use crate::types::{FieldValue, SeriesKey};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
46pub enum ColumnRole {
47 Timestamp,
49 Tag,
51 Field,
53}
54
55impl fmt::Display for ColumnRole {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 Self::Timestamp => write!(f, "timestamp"),
59 Self::Tag => write!(f, "tag"),
60 Self::Field => write!(f, "field"),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub enum ColumnType {
68 Timestamp,
70 String,
72 F64,
74 I64,
76 U64,
78 Bool,
80 Decimal {
95 scale: u8,
97 },
98}
99
100impl ColumnType {
101 #[inline]
108 #[must_use]
109 pub fn from_field_value(value: &FieldValue) -> Self {
110 match value {
111 FieldValue::F64(_) => Self::F64,
112 FieldValue::I64(_) => Self::I64,
113 FieldValue::U64(_) => Self::U64,
114 FieldValue::Bool(_) => Self::Bool,
115 FieldValue::String(_) => Self::String,
116 FieldValue::Decimal(d) => Self::Decimal { scale: d.scale() },
117 }
118 }
119
120 #[must_use]
128 pub fn accepts_value(&self, value: &FieldValue) -> bool {
129 match (self, value) {
130 (Self::Decimal { scale }, FieldValue::Decimal(d)) => d.rescale(*scale).is_ok(),
131 _ => *self == Self::from_field_value(value),
132 }
133 }
134
135 #[inline]
137 #[must_use]
138 pub const fn decimal_scale(&self) -> Option<u8> {
139 match self {
140 Self::Decimal { scale } => Some(*scale),
141 _ => None,
142 }
143 }
144}
145
146impl fmt::Display for ColumnType {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 match self {
149 Self::Timestamp => write!(f, "timestamp"),
150 Self::String => write!(f, "string"),
151 Self::F64 => write!(f, "f64"),
152 Self::I64 => write!(f, "i64"),
153 Self::U64 => write!(f, "u64"),
154 Self::Bool => write!(f, "bool"),
155 Self::Decimal { scale } => {
160 write!(f, "decimal({}, {scale})", crate::decimal::DECIMAL_PRECISION)
161 }
162 }
163 }
164}
165
166impl std::str::FromStr for ColumnType {
167 type Err = SchemaError;
168
169 fn from_str(s: &str) -> Result<Self, Self::Err> {
177 let lower = s.trim().to_ascii_lowercase();
178 match lower.as_str() {
179 "timestamp" => return Ok(Self::Timestamp),
180 "string" | "utf8" => return Ok(Self::String),
181 "f64" | "float64" | "double" => return Ok(Self::F64),
182 "i64" | "int64" => return Ok(Self::I64),
183 "u64" | "uint64" => return Ok(Self::U64),
184 "bool" | "boolean" => return Ok(Self::Bool),
185 "decimal" => return Ok(Self::Decimal { scale: 0 }),
186 _ => {}
187 }
188
189 let invalid = || SchemaError::InvalidName {
193 name: s.to_string(),
194 reason: "not a column type: expected timestamp, string, f64, i64, u64, bool, \
195 or decimal(38, <scale>)"
196 .to_string(),
197 };
198 let args = lower
199 .strip_prefix("decimal(")
200 .and_then(|rest| rest.strip_suffix(')'))
201 .ok_or_else(invalid)?;
202 let (precision, scale) = args.split_once(',').ok_or_else(invalid)?;
203 let precision: u8 = precision.trim().parse().map_err(|_| invalid())?;
204 if precision != crate::decimal::DECIMAL_PRECISION {
205 return Err(SchemaError::InvalidName {
206 name: s.to_string(),
207 reason: format!(
208 "decimal precision must be {}, the only one this format stores",
209 crate::decimal::DECIMAL_PRECISION
210 ),
211 });
212 }
213 let scale: u8 = scale.trim().parse().map_err(|_| invalid())?;
214 if scale > crate::decimal::MAX_DECIMAL_SCALE {
215 return Err(SchemaError::InvalidName {
216 name: s.to_string(),
217 reason: format!(
218 "decimal scale {scale} exceeds the maximum of {}",
219 crate::decimal::MAX_DECIMAL_SCALE
220 ),
221 });
222 }
223 Ok(Self::Decimal { scale })
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229pub struct ColumnDef {
230 pub name: String,
232 pub column_type: ColumnType,
234 pub role: ColumnRole,
236}
237
238#[derive(Debug, Clone, Serialize)]
245pub struct MeasurementSchema {
246 measurement: String,
247 columns: Vec<ColumnDef>,
248 #[serde(skip)]
250 column_index: HashMap<String, usize>,
251}
252
253impl PartialEq for MeasurementSchema {
254 fn eq(&self, other: &Self) -> bool {
255 self.measurement == other.measurement && self.columns == other.columns
256 }
257}
258
259impl Eq for MeasurementSchema {}
260
261impl<'de> serde::Deserialize<'de> for MeasurementSchema {
262 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
263 where
264 D: serde::Deserializer<'de>,
265 {
266 #[derive(Deserialize)]
267 struct Helper {
268 measurement: String,
269 columns: Vec<ColumnDef>,
270 }
271 let h = Helper::deserialize(deserializer)?;
272 let column_index = h
273 .columns
274 .iter()
275 .enumerate()
276 .map(|(i, c)| (c.name.clone(), i))
277 .collect();
278 Ok(Self {
279 measurement: h.measurement,
280 columns: h.columns,
281 column_index,
282 })
283 }
284}
285
286impl MeasurementSchema {
287 #[must_use]
289 pub fn new(measurement: impl Into<String>) -> Self {
290 let ts_col = ColumnDef {
291 name: crate::TIME_COLUMN.to_string(),
292 column_type: ColumnType::Timestamp,
293 role: ColumnRole::Timestamp,
294 };
295 let mut column_index = HashMap::new();
296 column_index.insert(ts_col.name.clone(), 0);
297 Self {
298 measurement: measurement.into(),
299 columns: vec![ts_col],
300 column_index,
301 }
302 }
303
304 #[inline]
306 #[must_use]
307 pub fn measurement(&self) -> &str {
308 &self.measurement
309 }
310
311 #[inline]
313 #[must_use]
314 pub fn columns(&self) -> &[ColumnDef] {
315 &self.columns
316 }
317
318 #[must_use]
324 pub fn add_tag(&mut self, name: &str) -> bool {
325 if let Some(existing) = self.columns.iter().find(|c| c.name == name) {
326 if existing.role != ColumnRole::Tag {
327 tracing::error!(
330 measurement = %self.measurement,
331 column = name,
332 existing_role = ?existing.role,
333 "tag name conflicts with existing column role — rejecting"
334 );
335 }
336 return false;
337 }
338 let idx = self.columns.len();
339 self.columns.push(ColumnDef {
340 name: name.to_string(),
341 column_type: ColumnType::String,
342 role: ColumnRole::Tag,
343 });
344 self.column_index.insert(name.to_string(), idx);
345 true
346 }
347
348 pub fn add_field(&mut self, name: &str, field_value: &FieldValue) -> Result<bool, SchemaError> {
355 let new_type = ColumnType::from_field_value(field_value);
356
357 if let Some(existing) = self.columns.iter().find(|c| c.name == name) {
358 if existing.role != ColumnRole::Field {
360 return Err(SchemaError::TypeConflict {
361 measurement: self.measurement.clone(),
362 field: name.to_string(),
363 expected: format!("{:?}", existing.role),
364 got: "Field".to_string(),
365 });
366 }
367 if !existing.column_type.accepts_value(field_value) {
368 return Err(Self::mismatch(
369 &self.measurement,
370 name,
371 existing.column_type,
372 field_value,
373 ));
374 }
375 return Ok(false);
376 }
377
378 let idx = self.columns.len();
379 self.columns.push(ColumnDef {
380 name: name.to_string(),
381 column_type: new_type,
382 role: ColumnRole::Field,
383 });
384 self.column_index.insert(name.to_string(), idx);
385 Ok(true)
386 }
387
388 fn mismatch(
396 measurement: &str,
397 field: &str,
398 existing: ColumnType,
399 value: &FieldValue,
400 ) -> SchemaError {
401 match (existing, value) {
402 (ColumnType::Decimal { scale }, FieldValue::Decimal(d)) if d.scale() <= scale => {
408 SchemaError::InvalidFieldValue {
409 field: field.to_string(),
410 reason: format!(
411 "{d} does not fit decimal({precision}, {scale}): it needs {needed} \
412 significant digits at that scale, and the maximum is {precision}",
413 precision = crate::decimal::DECIMAL_PRECISION,
414 needed = d.mantissa().unsigned_abs().to_string().len()
415 + usize::from(scale - d.scale()),
416 ),
417 }
418 }
419 (ColumnType::Decimal { scale }, FieldValue::Decimal(d)) => {
420 SchemaError::DecimalScaleConflict {
421 measurement: measurement.to_string(),
422 field: field.to_string(),
423 declared: scale,
424 got: d.scale(),
425 value: d.to_string(),
426 }
427 }
428 _ => SchemaError::TypeConflict {
429 measurement: measurement.to_string(),
430 field: field.to_string(),
431 expected: existing.to_string(),
432 got: ColumnType::from_field_value(value).to_string(),
433 },
434 }
435 }
436
437 pub fn declare_field(
454 &mut self,
455 name: &str,
456 column_type: ColumnType,
457 ) -> Result<bool, SchemaError> {
458 if let Some(existing) = self.columns.iter().find(|c| c.name == name) {
459 if existing.role != ColumnRole::Field {
460 return Err(SchemaError::TypeConflict {
461 measurement: self.measurement.clone(),
462 field: name.to_string(),
463 expected: format!("{:?}", existing.role),
464 got: "Field".to_string(),
465 });
466 }
467 if existing.column_type != column_type {
468 return Err(SchemaError::TypeConflict {
469 measurement: self.measurement.clone(),
470 field: name.to_string(),
471 expected: existing.column_type.to_string(),
472 got: column_type.to_string(),
473 });
474 }
475 return Ok(false);
476 }
477 SeriesKey::validate_name(name, "field name")?;
478 if let ColumnType::Decimal { scale } = column_type {
479 if scale > crate::decimal::MAX_DECIMAL_SCALE {
480 return Err(SchemaError::InvalidFieldValue {
481 field: name.to_string(),
482 reason: format!(
483 "decimal scale {scale} exceeds the maximum of {}",
484 crate::decimal::MAX_DECIMAL_SCALE
485 ),
486 });
487 }
488 }
489 let idx = self.columns.len();
490 self.columns.push(ColumnDef {
491 name: name.to_string(),
492 column_type,
493 role: ColumnRole::Field,
494 });
495 self.column_index.insert(name.to_string(), idx);
496 Ok(true)
497 }
498
499 #[inline]
501 #[must_use]
502 pub fn column(&self, name: &str) -> Option<&ColumnDef> {
503 self.column_index
504 .get(name)
505 .and_then(|&i| self.columns.get(i))
506 }
507
508 pub fn push_column(&mut self, col: ColumnDef) {
512 if self.column_index.contains_key(&col.name) {
513 return; }
515 let idx = self.columns.len();
516 self.column_index.insert(col.name.clone(), idx);
517 self.columns.push(col);
518 }
519
520 #[must_use]
522 pub fn tag_count(&self) -> usize {
523 self.columns
524 .iter()
525 .filter(|c| c.role == ColumnRole::Tag)
526 .count()
527 }
528
529 #[must_use]
531 pub fn field_count(&self) -> usize {
532 self.columns
533 .iter()
534 .filter(|c| c.role == ColumnRole::Field)
535 .count()
536 }
537
538 #[must_use]
540 pub fn tag_names(&self) -> Vec<&str> {
541 self.columns
542 .iter()
543 .filter(|c| c.role == ColumnRole::Tag)
544 .map(|c| c.name.as_str())
545 .collect()
546 }
547
548 #[must_use]
550 pub fn field_names(&self) -> Vec<&str> {
551 self.columns
552 .iter()
553 .filter(|c| c.role == ColumnRole::Field)
554 .map(|c| c.name.as_str())
555 .collect()
556 }
557}
558
559#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
561pub enum SchemaAction {
562 CreateMeasurement(MeasurementSchema),
564 AddColumn {
566 measurement: String,
568 column: ColumnDef,
570 },
571}
572
573#[derive(Debug, Clone)]
594pub struct SchemaRegistry {
595 schemas: Arc<DashMap<String, Arc<MeasurementSchema>>>,
598 changes: Arc<std::sync::Mutex<()>>,
607}
608
609impl SchemaRegistry {
610 #[must_use]
612 pub fn new() -> Self {
613 Self {
614 schemas: Arc::new(DashMap::new()),
615 changes: Arc::new(std::sync::Mutex::new(())),
616 }
617 }
618
619 pub fn register_batch(
639 &self,
640 points: &[&crate::types::Point],
641 ) -> Result<Vec<SchemaAction>, SchemaError> {
642 let mut all_known = true;
644 for point in points {
645 match self.schemas.get(point.series_key().measurement()) {
646 Some(schema) => {
647 if !Self::point_fits(&schema, point)? {
648 all_known = false;
649 }
650 }
651 None => all_known = false,
652 }
653 }
654 if all_known {
655 return Ok(Vec::new());
656 }
657
658 let _guard = self
661 .changes
662 .lock()
663 .unwrap_or_else(std::sync::PoisonError::into_inner);
664 let mut pending: std::collections::BTreeMap<String, MeasurementSchema> =
665 std::collections::BTreeMap::new();
666 let mut actions = Vec::new();
667
668 let widest = Self::widest_decimal_scales(points);
674
675 for point in points {
676 let name = point.series_key().measurement();
677 let schema = match pending.get_mut(name) {
678 Some(s) => s,
679 None => {
680 let fresh = match self.schemas.get(name) {
681 Some(existing) => (**existing).clone(),
682 None => {
683 let s = MeasurementSchema::new(name);
684 actions.push(SchemaAction::CreateMeasurement(s.clone()));
685 s
686 }
687 };
688 pending.entry(name.to_string()).or_insert(fresh)
689 }
690 };
691 for tag_key in point.series_key().tag_keys() {
692 if schema.add_tag(tag_key) {
693 actions.push(SchemaAction::AddColumn {
694 measurement: name.to_string(),
695 column: ColumnDef {
696 name: tag_key.to_string(),
697 column_type: ColumnType::String,
698 role: ColumnRole::Tag,
699 },
700 });
701 }
702 }
703 for (field_name, field_value) in point.fields() {
704 let column_type = match ColumnType::from_field_value(field_value) {
705 ColumnType::Decimal { scale } => ColumnType::Decimal {
706 scale: widest
707 .get(&(name, field_name.as_ref()))
708 .copied()
709 .unwrap_or(scale),
710 },
711 other => other,
712 };
713 match schema.column(field_name.as_ref()) {
714 Some(existing) => {
715 if !existing.column_type.accepts_value(field_value) {
716 return Err(MeasurementSchema::mismatch(
717 name,
718 field_name.as_ref(),
719 existing.column_type,
720 field_value,
721 ));
722 }
723 }
724 None => {
725 if schema.declare_field(field_name.as_ref(), column_type)? {
726 actions.push(SchemaAction::AddColumn {
727 measurement: name.to_string(),
728 column: ColumnDef {
729 name: field_name.to_string(),
730 column_type,
731 role: ColumnRole::Field,
732 },
733 });
734 }
735 }
736 }
737 }
738 }
739
740 for (name, schema) in pending {
741 self.schemas.insert(name, Arc::new(schema));
742 }
743 Ok(actions)
744 }
745
746 fn widest_decimal_scales<'a>(
749 points: &[&'a crate::types::Point],
750 ) -> HashMap<(&'a str, &'a str), u8> {
751 let mut widest: HashMap<(&str, &str), u8> = HashMap::new();
752 for point in points {
753 for (field_name, field_value) in point.fields() {
754 if let FieldValue::Decimal(d) = field_value {
755 let key = (point.series_key().measurement(), field_name.as_ref());
756 let entry = widest.entry(key).or_insert(0);
757 *entry = (*entry).max(d.scale());
758 }
759 }
760 }
761 widest
762 }
763
764 pub fn normalize_decimals(
786 &self,
787 points: &[&crate::types::Point],
788 ) -> Result<Option<Vec<crate::types::Point>>, SchemaError> {
789 if !points
792 .iter()
793 .any(|p| p.fields().iter().any(|(_, v)| v.is_decimal()))
794 {
795 return Ok(None);
796 }
797
798 let mut out: Vec<crate::types::Point> = Vec::with_capacity(points.len());
799 for point in points {
800 let mut rewritten: Option<crate::types::Point> = None;
801 let schema = self.schemas.get(point.series_key().measurement());
802 for (field_name, field_value) in point.fields() {
803 let FieldValue::Decimal(value) = field_value else {
804 continue;
805 };
806 let Some(scale) = schema
807 .as_ref()
808 .and_then(|s| s.column(field_name.as_ref()))
809 .and_then(|c| c.column_type.decimal_scale())
810 else {
811 continue;
812 };
813 if scale == value.scale() {
814 continue;
815 }
816 let rescaled = value.rescale(scale).map_err(|_| {
817 MeasurementSchema::mismatch(
822 point.series_key().measurement(),
823 field_name.as_ref(),
824 ColumnType::Decimal { scale },
825 field_value,
826 )
827 })?;
828 rewritten
829 .get_or_insert_with(|| (*point).clone())
830 .set_field(field_name.as_ref(), FieldValue::Decimal(rescaled));
831 }
832 out.push(rewritten.unwrap_or_else(|| (*point).clone()));
833 }
834 Ok(Some(out))
835 }
836
837 pub fn declare_field(
853 &self,
854 measurement: &str,
855 field: &str,
856 column_type: ColumnType,
857 ) -> Result<Vec<SchemaAction>, SchemaError> {
858 let _guard = self
859 .changes
860 .lock()
861 .unwrap_or_else(std::sync::PoisonError::into_inner);
862
863 let mut actions = Vec::new();
864 let mut schema = match self.schemas.get(measurement) {
865 Some(existing) => (**existing).clone(),
866 None => {
867 let fresh = MeasurementSchema::new(measurement);
868 actions.push(SchemaAction::CreateMeasurement(fresh.clone()));
869 fresh
870 }
871 };
872 if schema.declare_field(field, column_type)? {
873 actions.push(SchemaAction::AddColumn {
874 measurement: measurement.to_string(),
875 column: ColumnDef {
876 name: field.to_string(),
877 column_type,
878 role: ColumnRole::Field,
879 },
880 });
881 } else if actions.is_empty() {
882 return Ok(Vec::new());
884 }
885 self.schemas
886 .insert(measurement.to_string(), Arc::new(schema));
887 Ok(actions)
888 }
889
890 fn point_fits(
893 schema: &MeasurementSchema,
894 point: &crate::types::Point,
895 ) -> Result<bool, SchemaError> {
896 for tag_key in point.series_key().tag_keys() {
897 if schema.column(tag_key).is_none() {
898 return Ok(false);
899 }
900 }
901 for (field_name, field_value) in point.fields() {
902 match schema.column(field_name.as_ref()) {
903 Some(existing) => {
904 if !existing.column_type.accepts_value(field_value) {
905 return Err(MeasurementSchema::mismatch(
906 schema.measurement(),
907 field_name.as_ref(),
908 existing.column_type,
909 field_value,
910 ));
911 }
912 }
913 None => return Ok(false),
914 }
915 }
916 Ok(true)
917 }
918
919 pub fn register_point(
927 &self,
928 point: &crate::types::Point,
929 ) -> Result<Vec<SchemaAction>, SchemaError> {
930 self.register_batch(&[point])
931 }
932
933 #[must_use]
937 pub fn lookup(&self, measurement: &str) -> Option<Arc<MeasurementSchema>> {
938 self.schemas.get(measurement).map(|r| r.value().clone())
939 }
940
941 pub fn register_schema(&self, name: &str, schema: MeasurementSchema) {
945 self.schemas
946 .entry(name.to_string())
947 .or_insert(Arc::new(schema));
948 }
949
950 pub fn register_measurement(&self, ms: MeasurementSchema) {
954 self.schemas
955 .entry(ms.measurement().to_string())
956 .or_insert(Arc::new(ms));
957 }
958
959 pub fn apply_add_column(&self, measurement: &str, column: ColumnDef) {
964 if let Some(mut schema_arc) = self.schemas.get_mut(measurement) {
965 if let Some(existing) = schema_arc
966 .value()
967 .columns()
968 .iter()
969 .find(|c| c.name == column.name)
970 {
971 if existing.role != column.role {
973 tracing::warn!(
974 measurement,
975 column = %column.name,
976 existing_role = ?existing.role,
977 new_role = ?column.role,
978 "WAL replay column role conflict — skipping"
979 );
980 }
981 return;
983 }
984 let schema = Arc::make_mut(schema_arc.value_mut());
985 match column.role {
986 ColumnRole::Tag => {
987 let _ = schema.add_tag(&column.name);
988 }
989 ColumnRole::Field => {
990 schema.push_column(column);
993 }
994 ColumnRole::Timestamp => {
995 }
997 }
998 }
999 }
1000
1001 #[must_use]
1003 pub fn measurement_count(&self) -> usize {
1004 self.schemas.len()
1005 }
1006
1007 #[must_use]
1009 pub fn measurement_names(&self) -> Vec<String> {
1010 self.schemas.iter().map(|r| r.key().clone()).collect()
1011 }
1012
1013 #[must_use]
1015 pub fn remove(&self, measurement: &str) -> Option<Arc<MeasurementSchema>> {
1016 self.schemas.remove(measurement).map(|(_, v)| v)
1017 }
1018}
1019
1020impl Default for SchemaRegistry {
1021 fn default() -> Self {
1022 Self::new()
1023 }
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028 use super::*;
1029 use crate::types::{FieldValue, Point, SeriesKey};
1030 use std::collections::BTreeMap;
1031
1032 fn make_point(
1033 measurement: &str,
1034 tags: &[(&str, &str)],
1035 fields: &[(&str, FieldValue)],
1036 ts: i64,
1037 ) -> Point {
1038 let tag_map: BTreeMap<String, String> = tags
1039 .iter()
1040 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1041 .collect();
1042 let field_map: BTreeMap<String, FieldValue> = fields
1043 .iter()
1044 .map(|(k, v)| ((*k).to_string(), v.clone()))
1045 .collect();
1046 let key = SeriesKey::new(measurement, tag_map).unwrap();
1047 Point::new(key, field_map, ts).unwrap()
1048 }
1049
1050 #[test]
1051 fn measurement_schema_new() {
1052 let schema = MeasurementSchema::new("cpu");
1053 assert_eq!(schema.measurement(), "cpu");
1054 assert_eq!(schema.columns().len(), 1);
1055 assert_eq!(schema.columns()[0].name, crate::TIME_COLUMN);
1056 assert_eq!(schema.columns()[0].role, ColumnRole::Timestamp);
1057 }
1058
1059 #[test]
1060 fn measurement_schema_add_tag() {
1061 let mut schema = MeasurementSchema::new("cpu");
1062 assert!(schema.add_tag("host"));
1063 assert!(!schema.add_tag("host")); assert_eq!(schema.tag_count(), 1);
1065 assert_eq!(schema.columns()[1].name, "host");
1066 assert_eq!(schema.columns()[1].role, ColumnRole::Tag);
1067 }
1068
1069 #[test]
1070 fn measurement_schema_add_field() {
1071 let mut schema = MeasurementSchema::new("cpu");
1072 let added = schema.add_field("value", &FieldValue::F64(1.0)).unwrap();
1073 assert!(added);
1074 let added = schema.add_field("value", &FieldValue::F64(2.0)).unwrap();
1075 assert!(!added); assert_eq!(schema.field_count(), 1);
1077 }
1078
1079 #[test]
1080 fn measurement_schema_type_conflict() {
1081 let mut schema = MeasurementSchema::new("cpu");
1082 schema.add_field("value", &FieldValue::F64(1.0)).unwrap();
1083 let err = schema.add_field("value", &FieldValue::I64(1)).unwrap_err();
1084 assert!(matches!(err, SchemaError::TypeConflict { .. }));
1085 }
1086
1087 #[test]
1088 fn schema_registry_auto_create() {
1089 let registry = SchemaRegistry::new();
1090 let point = make_point(
1091 "cpu",
1092 &[("host", "srv1")],
1093 &[("value", FieldValue::F64(72.5))],
1094 1_000_000_000,
1095 );
1096 let actions = registry.register_point(&point).unwrap();
1097 assert_eq!(actions.len(), 3);
1099 assert!(matches!(&actions[0], SchemaAction::CreateMeasurement(_)));
1100
1101 let schema = registry.lookup("cpu").unwrap();
1102 assert_eq!(schema.measurement(), "cpu");
1103 assert_eq!(schema.tag_count(), 1);
1104 assert_eq!(schema.field_count(), 1);
1105 }
1106
1107 #[test]
1108 fn schema_registry_additive_evolution() {
1109 let registry = SchemaRegistry::new();
1110
1111 let p1 = make_point(
1113 "cpu",
1114 &[("host", "srv1")],
1115 &[("value", FieldValue::F64(72.5))],
1116 1,
1117 );
1118 registry.register_point(&p1).unwrap();
1119
1120 let p2 = make_point(
1122 "cpu",
1123 &[("host", "srv1"), ("dc", "eu")],
1124 &[
1125 ("value", FieldValue::F64(73.0)),
1126 ("cores", FieldValue::I64(8)),
1127 ],
1128 2,
1129 );
1130 let actions = registry.register_point(&p2).unwrap();
1131 assert_eq!(actions.len(), 2);
1133
1134 let schema = registry.lookup("cpu").unwrap();
1135 assert_eq!(schema.tag_count(), 2);
1136 assert_eq!(schema.field_count(), 2);
1137 }
1138
1139 #[test]
1140 fn schema_registry_type_conflict_rejected() {
1141 let registry = SchemaRegistry::new();
1142
1143 let p1 = make_point("cpu", &[], &[("value", FieldValue::F64(72.5))], 1);
1144 registry.register_point(&p1).unwrap();
1145
1146 let p2 = make_point("cpu", &[], &[("value", FieldValue::I64(72))], 2);
1147 let err = registry.register_point(&p2).unwrap_err();
1148 assert!(matches!(err, SchemaError::TypeConflict { .. }));
1149 }
1150
1151 #[test]
1152 fn schema_registry_multiple_measurements() {
1153 let registry = SchemaRegistry::new();
1154
1155 let p1 = make_point("cpu", &[], &[("value", FieldValue::F64(72.5))], 1);
1156 let p2 = make_point("mem", &[], &[("used", FieldValue::U64(1024))], 1);
1157
1158 registry.register_point(&p1).unwrap();
1159 registry.register_point(&p2).unwrap();
1160
1161 assert_eq!(registry.measurement_count(), 2);
1162 assert!(registry.lookup("cpu").is_some());
1163 assert!(registry.lookup("mem").is_some());
1164 assert!(registry.lookup("disk").is_none());
1165 }
1166
1167 #[test]
1168 fn schema_registry_remove() {
1169 let registry = SchemaRegistry::new();
1170 let p = make_point("cpu", &[], &[("value", FieldValue::F64(1.0))], 1);
1171 registry.register_point(&p).unwrap();
1172 assert!(registry.lookup("cpu").is_some());
1173
1174 let removed = registry.remove("cpu");
1175 assert!(removed.is_some());
1176 assert!(registry.lookup("cpu").is_none());
1177 assert_eq!(registry.measurement_count(), 0);
1178 }
1179
1180 #[test]
1181 fn schema_registry_measurement_names() {
1182 let registry = SchemaRegistry::new();
1183 let p1 = make_point("cpu", &[], &[("v", FieldValue::F64(1.0))], 1);
1184 let p2 = make_point("mem", &[], &[("v", FieldValue::U64(1))], 1);
1185 registry.register_point(&p1).unwrap();
1186 registry.register_point(&p2).unwrap();
1187
1188 let mut names = registry.measurement_names();
1189 names.sort();
1190 assert_eq!(names, vec!["cpu", "mem"]);
1191 }
1192
1193 #[test]
1194 fn schema_serde_roundtrip() {
1195 let mut schema = MeasurementSchema::new("cpu");
1196 let _ = schema.add_tag("host");
1197 schema.add_field("value", &FieldValue::F64(1.0)).unwrap();
1198
1199 let json = serde_json::to_string(&schema).unwrap();
1200 let back: MeasurementSchema = serde_json::from_str(&json).unwrap();
1201 assert_eq!(schema, back);
1202 }
1203
1204 #[test]
1205 fn a_decimal_column_accepts_a_narrower_value_and_refuses_a_finer_one() {
1206 let mut schema = MeasurementSchema::new("meter");
1207 schema
1208 .declare_field("z1nb", ColumnType::Decimal { scale: 4 })
1209 .unwrap();
1210 assert!(!schema
1212 .add_field("z1nb", &FieldValue::Decimal("1.5".parse().unwrap()))
1213 .unwrap());
1214 assert!(!schema
1216 .add_field("z1nb", &FieldValue::Decimal("1.50000".parse().unwrap()))
1217 .unwrap());
1218 let err = schema
1220 .add_field("z1nb", &FieldValue::Decimal("1.00005".parse().unwrap()))
1221 .unwrap_err();
1222 assert!(
1223 matches!(
1224 err,
1225 SchemaError::DecimalScaleConflict {
1226 declared: 4,
1227 got: 5,
1228 ..
1229 }
1230 ),
1231 "{err:?}"
1232 );
1233 }
1234
1235 #[test]
1236 fn running_out_of_digits_is_not_reported_as_a_scale_conflict() {
1237 let mut schema = MeasurementSchema::new("m");
1241 schema
1242 .declare_field("v", ColumnType::Decimal { scale: 4 })
1243 .unwrap();
1244 let huge = crate::decimal::Decimal::new(10_i128.pow(37), 0).unwrap();
1245 let err = schema
1246 .add_field("v", &FieldValue::Decimal(huge))
1247 .unwrap_err();
1248 match err {
1249 SchemaError::InvalidFieldValue { ref reason, .. } => {
1250 assert!(reason.contains("significant digits"), "{reason}");
1251 }
1252 other => panic!("expected a precision error, got {other:?}"),
1253 }
1254 }
1255
1256 #[test]
1257 fn a_decimal_and_another_type_are_still_a_type_conflict() {
1258 let mut schema = MeasurementSchema::new("m");
1259 schema
1260 .declare_field("v", ColumnType::Decimal { scale: 2 })
1261 .unwrap();
1262 let err = schema.add_field("v", &FieldValue::F64(1.5)).unwrap_err();
1263 assert!(matches!(err, SchemaError::TypeConflict { .. }), "{err:?}");
1264 }
1265
1266 #[test]
1267 fn a_batch_creating_a_decimal_column_takes_its_widest_scale() {
1268 let registry = SchemaRegistry::new();
1269 for order in [["1.5", "1.4999"], ["1.4999", "1.5"]] {
1271 let registry = registry.clone();
1272 let points: Vec<Point> = order
1273 .iter()
1274 .enumerate()
1275 .map(|(i, text)| {
1276 make_point(
1277 "meter",
1278 &[],
1279 &[("v", FieldValue::Decimal(text.parse().unwrap()))],
1280 i as i64,
1281 )
1282 })
1283 .collect();
1284 let refs: Vec<&Point> = points.iter().collect();
1285 registry.register_batch(&refs).unwrap();
1286 assert_eq!(
1287 registry
1288 .lookup("meter")
1289 .unwrap()
1290 .column("v")
1291 .unwrap()
1292 .column_type,
1293 ColumnType::Decimal { scale: 4 },
1294 "order {order:?}"
1295 );
1296 }
1297 }
1298
1299 #[test]
1300 fn normalize_decimals_widens_to_the_column_and_copies_nothing_otherwise() {
1301 let registry = SchemaRegistry::new();
1302 let point = make_point(
1303 "meter",
1304 &[],
1305 &[("v", FieldValue::Decimal("1.5000".parse().unwrap()))],
1306 1,
1307 );
1308 registry.register_batch(&[&point]).unwrap();
1309
1310 let plain = make_point("cpu", &[], &[("v", FieldValue::F64(1.0))], 1);
1312 assert!(registry.normalize_decimals(&[&plain]).unwrap().is_none());
1313
1314 let narrow = make_point(
1316 "meter",
1317 &[],
1318 &[("v", FieldValue::Decimal("1.5".parse().unwrap()))],
1319 2,
1320 );
1321 let out = registry.normalize_decimals(&[&narrow]).unwrap().unwrap();
1322 match out[0].field("v") {
1323 Some(FieldValue::Decimal(d)) => {
1324 assert_eq!(d.scale(), 4);
1325 assert_eq!(d.to_string(), "1.5000");
1326 }
1327 other => panic!("expected a decimal, got {other:?}"),
1328 }
1329 }
1330
1331 #[test]
1332 fn column_type_from_field_value() {
1333 assert_eq!(
1334 ColumnType::from_field_value(&FieldValue::F64(1.0)),
1335 ColumnType::F64
1336 );
1337 assert_eq!(
1338 ColumnType::from_field_value(&FieldValue::I64(1)),
1339 ColumnType::I64
1340 );
1341 assert_eq!(
1342 ColumnType::from_field_value(&FieldValue::U64(1)),
1343 ColumnType::U64
1344 );
1345 assert_eq!(
1346 ColumnType::from_field_value(&FieldValue::Bool(true)),
1347 ColumnType::Bool
1348 );
1349 assert_eq!(
1350 ColumnType::from_field_value(&FieldValue::String("x".into())),
1351 ColumnType::String
1352 );
1353 }
1354
1355 #[test]
1356 fn column_type_display() {
1357 assert_eq!(ColumnType::Timestamp.to_string(), "timestamp");
1358 assert_eq!(ColumnType::String.to_string(), "string");
1359 assert_eq!(ColumnType::F64.to_string(), "f64");
1360 assert_eq!(ColumnType::I64.to_string(), "i64");
1361 assert_eq!(ColumnType::U64.to_string(), "u64");
1362 assert_eq!(ColumnType::Bool.to_string(), "bool");
1363 }
1364
1365 #[test]
1366 fn column_role_display() {
1367 assert_eq!(ColumnRole::Timestamp.to_string(), "timestamp");
1368 assert_eq!(ColumnRole::Tag.to_string(), "tag");
1369 assert_eq!(ColumnRole::Field.to_string(), "field");
1370 }
1371
1372 #[test]
1373 fn measurement_schema_column_not_found() {
1374 let schema = MeasurementSchema::new("cpu");
1375 assert!(schema.column("nonexistent").is_none());
1376 }
1377
1378 #[test]
1379 fn schema_registry_remove_nonexistent() {
1380 let registry = SchemaRegistry::new();
1381 let removed = registry.remove("nope");
1382 assert!(removed.is_none());
1383 }
1384
1385 #[test]
1386 fn schema_action_serde_roundtrip() {
1387 let action = SchemaAction::CreateMeasurement(MeasurementSchema::new("cpu"));
1388 let json = serde_json::to_string(&action).unwrap();
1389 let back: SchemaAction = serde_json::from_str(&json).unwrap();
1390 assert_eq!(action, back);
1391
1392 let action = SchemaAction::AddColumn {
1393 measurement: "cpu".into(),
1394 column: ColumnDef {
1395 name: "value".into(),
1396 column_type: ColumnType::F64,
1397 role: ColumnRole::Field,
1398 },
1399 };
1400 let json = serde_json::to_string(&action).unwrap();
1401 let back: SchemaAction = serde_json::from_str(&json).unwrap();
1402 assert_eq!(action, back);
1403 }
1404
1405 #[test]
1406 fn measurement_schema_accessors() {
1407 let mut schema = MeasurementSchema::new("cpu");
1408 assert!(schema.add_tag("host"));
1409 assert_eq!(schema.measurement(), "cpu");
1410 assert_eq!(schema.columns().len(), 2);
1411 }
1412}