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 Histogram,
94
95 Decimal {
110 scale: u8,
112 },
113}
114
115impl ColumnType {
116 #[inline]
123 #[must_use]
124 pub fn from_field_value(value: &FieldValue) -> Self {
125 match value {
126 FieldValue::F64(_) => Self::F64,
127 FieldValue::I64(_) => Self::I64,
128 FieldValue::U64(_) => Self::U64,
129 FieldValue::Bool(_) => Self::Bool,
130 FieldValue::String(_) => Self::String,
131 FieldValue::Decimal(d) => Self::Decimal { scale: d.scale() },
132 FieldValue::Histogram(_) => Self::Histogram,
133 }
134 }
135
136 #[must_use]
144 pub fn accepts_value(&self, value: &FieldValue) -> bool {
145 match (self, value) {
146 (Self::Decimal { scale }, FieldValue::Decimal(d)) => d.rescale(*scale).is_ok(),
147 _ => *self == Self::from_field_value(value),
148 }
149 }
150
151 #[inline]
153 #[must_use]
154 pub const fn decimal_scale(&self) -> Option<u8> {
155 match self {
156 Self::Decimal { scale } => Some(*scale),
157 _ => None,
158 }
159 }
160}
161
162impl fmt::Display for ColumnType {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 match self {
165 Self::Timestamp => write!(f, "timestamp"),
166 Self::String => write!(f, "string"),
167 Self::F64 => write!(f, "f64"),
168 Self::I64 => write!(f, "i64"),
169 Self::U64 => write!(f, "u64"),
170 Self::Bool => write!(f, "bool"),
171 Self::Histogram => write!(f, "histogram"),
172 Self::Decimal { scale } => {
177 write!(f, "decimal({}, {scale})", crate::decimal::DECIMAL_PRECISION)
178 }
179 }
180 }
181}
182
183impl std::str::FromStr for ColumnType {
184 type Err = SchemaError;
185
186 fn from_str(s: &str) -> Result<Self, Self::Err> {
194 let lower = s.trim().to_ascii_lowercase();
195 match lower.as_str() {
196 "timestamp" => return Ok(Self::Timestamp),
197 "string" | "utf8" => return Ok(Self::String),
198 "f64" | "float64" | "double" => return Ok(Self::F64),
199 "i64" | "int64" => return Ok(Self::I64),
200 "u64" | "uint64" => return Ok(Self::U64),
201 "bool" | "boolean" => return Ok(Self::Bool),
202 "decimal" => return Ok(Self::Decimal { scale: 0 }),
203 _ => {}
204 }
205
206 let invalid = || SchemaError::InvalidName {
210 name: s.to_string(),
211 reason: "not a column type: expected timestamp, string, f64, i64, u64, bool, \
212 or decimal(38, <scale>)"
213 .to_string(),
214 };
215 let args = lower
216 .strip_prefix("decimal(")
217 .and_then(|rest| rest.strip_suffix(')'))
218 .ok_or_else(invalid)?;
219 let (precision, scale) = args.split_once(',').ok_or_else(invalid)?;
220 let precision: u8 = precision.trim().parse().map_err(|_| invalid())?;
221 if precision != crate::decimal::DECIMAL_PRECISION {
222 return Err(SchemaError::InvalidName {
223 name: s.to_string(),
224 reason: format!(
225 "decimal precision must be {}, the only one this format stores",
226 crate::decimal::DECIMAL_PRECISION
227 ),
228 });
229 }
230 let scale: u8 = scale.trim().parse().map_err(|_| invalid())?;
231 if scale > crate::decimal::MAX_DECIMAL_SCALE {
232 return Err(SchemaError::InvalidName {
233 name: s.to_string(),
234 reason: format!(
235 "decimal scale {scale} exceeds the maximum of {}",
236 crate::decimal::MAX_DECIMAL_SCALE
237 ),
238 });
239 }
240 Ok(Self::Decimal { scale })
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct ColumnDef {
247 pub name: String,
249 pub column_type: ColumnType,
251 pub role: ColumnRole,
253}
254
255#[derive(Debug, Clone, Serialize)]
262pub struct MeasurementSchema {
263 measurement: String,
264 columns: Vec<ColumnDef>,
265 #[serde(skip)]
267 column_index: HashMap<String, usize>,
268}
269
270impl PartialEq for MeasurementSchema {
271 fn eq(&self, other: &Self) -> bool {
272 self.measurement == other.measurement && self.columns == other.columns
273 }
274}
275
276impl Eq for MeasurementSchema {}
277
278impl<'de> serde::Deserialize<'de> for MeasurementSchema {
279 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
280 where
281 D: serde::Deserializer<'de>,
282 {
283 #[derive(Deserialize)]
284 struct Helper {
285 measurement: String,
286 columns: Vec<ColumnDef>,
287 }
288 let h = Helper::deserialize(deserializer)?;
289 let column_index = h
290 .columns
291 .iter()
292 .enumerate()
293 .map(|(i, c)| (c.name.clone(), i))
294 .collect();
295 Ok(Self {
296 measurement: h.measurement,
297 columns: h.columns,
298 column_index,
299 })
300 }
301}
302
303impl MeasurementSchema {
304 #[must_use]
306 pub fn new(measurement: impl Into<String>) -> Self {
307 let ts_col = ColumnDef {
308 name: crate::TIME_COLUMN.to_string(),
309 column_type: ColumnType::Timestamp,
310 role: ColumnRole::Timestamp,
311 };
312 let mut column_index = HashMap::new();
313 column_index.insert(ts_col.name.clone(), 0);
314 Self {
315 measurement: measurement.into(),
316 columns: vec![ts_col],
317 column_index,
318 }
319 }
320
321 #[inline]
323 #[must_use]
324 pub fn measurement(&self) -> &str {
325 &self.measurement
326 }
327
328 #[inline]
330 #[must_use]
331 pub fn columns(&self) -> &[ColumnDef] {
332 &self.columns
333 }
334
335 #[must_use]
341 pub fn add_tag(&mut self, name: &str) -> bool {
342 if let Some(existing) = self.columns.iter().find(|c| c.name == name) {
343 if existing.role != ColumnRole::Tag {
344 tracing::error!(
347 measurement = %self.measurement,
348 column = name,
349 existing_role = ?existing.role,
350 "tag name conflicts with existing column role — rejecting"
351 );
352 }
353 return false;
354 }
355 let idx = self.columns.len();
356 self.columns.push(ColumnDef {
357 name: name.to_string(),
358 column_type: ColumnType::String,
359 role: ColumnRole::Tag,
360 });
361 self.column_index.insert(name.to_string(), idx);
362 true
363 }
364
365 pub fn add_field(&mut self, name: &str, field_value: &FieldValue) -> Result<bool, SchemaError> {
372 let new_type = ColumnType::from_field_value(field_value);
373
374 if let Some(existing) = self.columns.iter().find(|c| c.name == name) {
375 if existing.role != ColumnRole::Field {
377 return Err(SchemaError::TypeConflict {
378 measurement: self.measurement.clone(),
379 field: name.to_string(),
380 expected: format!("{:?}", existing.role),
381 got: "Field".to_string(),
382 });
383 }
384 if !existing.column_type.accepts_value(field_value) {
385 return Err(Self::mismatch(
386 &self.measurement,
387 name,
388 existing.column_type,
389 field_value,
390 ));
391 }
392 return Ok(false);
393 }
394
395 let idx = self.columns.len();
396 self.columns.push(ColumnDef {
397 name: name.to_string(),
398 column_type: new_type,
399 role: ColumnRole::Field,
400 });
401 self.column_index.insert(name.to_string(), idx);
402 Ok(true)
403 }
404
405 fn mismatch(
413 measurement: &str,
414 field: &str,
415 existing: ColumnType,
416 value: &FieldValue,
417 ) -> SchemaError {
418 match (existing, value) {
419 (ColumnType::Decimal { scale }, FieldValue::Decimal(d)) if d.scale() <= scale => {
425 SchemaError::InvalidFieldValue {
426 field: field.to_string(),
427 reason: format!(
428 "{d} does not fit decimal({precision}, {scale}): it needs {needed} \
429 significant digits at that scale, and the maximum is {precision}",
430 precision = crate::decimal::DECIMAL_PRECISION,
431 needed = d.mantissa().unsigned_abs().to_string().len()
432 + usize::from(scale - d.scale()),
433 ),
434 }
435 }
436 (ColumnType::Decimal { scale }, FieldValue::Decimal(d)) => {
437 SchemaError::DecimalScaleConflict {
438 measurement: measurement.to_string(),
439 field: field.to_string(),
440 declared: scale,
441 got: d.scale(),
442 value: d.to_string(),
443 }
444 }
445 _ => SchemaError::TypeConflict {
446 measurement: measurement.to_string(),
447 field: field.to_string(),
448 expected: existing.to_string(),
449 got: ColumnType::from_field_value(value).to_string(),
450 },
451 }
452 }
453
454 pub fn declare_field(
471 &mut self,
472 name: &str,
473 column_type: ColumnType,
474 ) -> Result<bool, SchemaError> {
475 if let Some(existing) = self.columns.iter().find(|c| c.name == name) {
476 if existing.role != ColumnRole::Field {
477 return Err(SchemaError::TypeConflict {
478 measurement: self.measurement.clone(),
479 field: name.to_string(),
480 expected: format!("{:?}", existing.role),
481 got: "Field".to_string(),
482 });
483 }
484 if existing.column_type != column_type {
485 return Err(SchemaError::TypeConflict {
486 measurement: self.measurement.clone(),
487 field: name.to_string(),
488 expected: existing.column_type.to_string(),
489 got: column_type.to_string(),
490 });
491 }
492 return Ok(false);
493 }
494 SeriesKey::validate_name(name, "field name")?;
495 if let ColumnType::Decimal { scale } = column_type
496 && scale > crate::decimal::MAX_DECIMAL_SCALE
497 {
498 return Err(SchemaError::InvalidFieldValue {
499 field: name.to_string(),
500 reason: format!(
501 "decimal scale {scale} exceeds the maximum of {}",
502 crate::decimal::MAX_DECIMAL_SCALE
503 ),
504 });
505 }
506 let idx = self.columns.len();
507 self.columns.push(ColumnDef {
508 name: name.to_string(),
509 column_type,
510 role: ColumnRole::Field,
511 });
512 self.column_index.insert(name.to_string(), idx);
513 Ok(true)
514 }
515
516 #[inline]
518 #[must_use]
519 pub fn column(&self, name: &str) -> Option<&ColumnDef> {
520 self.column_index
521 .get(name)
522 .and_then(|&i| self.columns.get(i))
523 }
524
525 pub fn push_column(&mut self, col: ColumnDef) {
529 if self.column_index.contains_key(&col.name) {
530 return; }
532 let idx = self.columns.len();
533 self.column_index.insert(col.name.clone(), idx);
534 self.columns.push(col);
535 }
536
537 #[must_use]
539 pub fn tag_count(&self) -> usize {
540 self.columns
541 .iter()
542 .filter(|c| c.role == ColumnRole::Tag)
543 .count()
544 }
545
546 #[must_use]
548 pub fn field_count(&self) -> usize {
549 self.columns
550 .iter()
551 .filter(|c| c.role == ColumnRole::Field)
552 .count()
553 }
554
555 #[must_use]
557 pub fn tag_names(&self) -> Vec<&str> {
558 self.columns
559 .iter()
560 .filter(|c| c.role == ColumnRole::Tag)
561 .map(|c| c.name.as_str())
562 .collect()
563 }
564
565 #[must_use]
567 pub fn field_names(&self) -> Vec<&str> {
568 self.columns
569 .iter()
570 .filter(|c| c.role == ColumnRole::Field)
571 .map(|c| c.name.as_str())
572 .collect()
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
578pub enum SchemaAction {
579 CreateMeasurement(MeasurementSchema),
581 AddColumn {
583 measurement: String,
585 column: ColumnDef,
587 },
588}
589
590#[derive(Debug, Clone)]
611pub struct SchemaRegistry {
612 schemas: Arc<DashMap<String, Arc<MeasurementSchema>>>,
615 changes: Arc<std::sync::Mutex<()>>,
624}
625
626impl SchemaRegistry {
627 #[must_use]
629 pub fn new() -> Self {
630 Self {
631 schemas: Arc::new(DashMap::new()),
632 changes: Arc::new(std::sync::Mutex::new(())),
633 }
634 }
635
636 pub fn register_batch(
656 &self,
657 points: &[&crate::types::Point],
658 ) -> Result<Vec<SchemaAction>, SchemaError> {
659 let mut all_known = true;
661 for point in points {
662 match self.schemas.get(point.series_key().measurement()) {
663 Some(schema) => {
664 if !Self::point_fits(&schema, point)? {
665 all_known = false;
666 }
667 }
668 None => all_known = false,
669 }
670 }
671 if all_known {
672 return Ok(Vec::new());
673 }
674
675 let _guard = self
678 .changes
679 .lock()
680 .unwrap_or_else(std::sync::PoisonError::into_inner);
681 let mut pending: std::collections::BTreeMap<String, MeasurementSchema> =
682 std::collections::BTreeMap::new();
683 let mut actions = Vec::new();
684
685 let widest = Self::widest_decimal_scales(points);
691
692 for point in points {
693 let name = point.series_key().measurement();
694 let schema = match pending.get_mut(name) {
695 Some(s) => s,
696 None => {
697 let fresh = match self.schemas.get(name) {
698 Some(existing) => (**existing).clone(),
699 None => {
700 let s = MeasurementSchema::new(name);
701 actions.push(SchemaAction::CreateMeasurement(s.clone()));
702 s
703 }
704 };
705 pending.entry(name.to_string()).or_insert(fresh)
706 }
707 };
708 for tag_key in point.series_key().tag_keys() {
709 if schema.add_tag(tag_key) {
710 actions.push(SchemaAction::AddColumn {
711 measurement: name.to_string(),
712 column: ColumnDef {
713 name: tag_key.to_string(),
714 column_type: ColumnType::String,
715 role: ColumnRole::Tag,
716 },
717 });
718 }
719 }
720 for (field_name, field_value) in point.fields() {
721 let column_type = match ColumnType::from_field_value(field_value) {
722 ColumnType::Decimal { scale } => ColumnType::Decimal {
723 scale: widest
724 .get(&(name, field_name.as_ref()))
725 .copied()
726 .unwrap_or(scale),
727 },
728 other => other,
729 };
730 match schema.column(field_name.as_ref()) {
731 Some(existing) => {
732 if !existing.column_type.accepts_value(field_value) {
733 return Err(MeasurementSchema::mismatch(
734 name,
735 field_name.as_ref(),
736 existing.column_type,
737 field_value,
738 ));
739 }
740 }
741 None => {
742 if schema.declare_field(field_name.as_ref(), column_type)? {
743 actions.push(SchemaAction::AddColumn {
744 measurement: name.to_string(),
745 column: ColumnDef {
746 name: field_name.to_string(),
747 column_type,
748 role: ColumnRole::Field,
749 },
750 });
751 }
752 }
753 }
754 }
755 }
756
757 for (name, schema) in pending {
758 self.schemas.insert(name, Arc::new(schema));
759 }
760 Ok(actions)
761 }
762
763 fn widest_decimal_scales<'a>(
766 points: &[&'a crate::types::Point],
767 ) -> HashMap<(&'a str, &'a str), u8> {
768 let mut widest: HashMap<(&str, &str), u8> = HashMap::new();
769 for point in points {
770 for (field_name, field_value) in point.fields() {
771 if let FieldValue::Decimal(d) = field_value {
772 let key = (point.series_key().measurement(), field_name.as_ref());
773 let entry = widest.entry(key).or_insert(0);
774 *entry = (*entry).max(d.scale());
775 }
776 }
777 }
778 widest
779 }
780
781 pub fn normalize_decimals(
803 &self,
804 points: &[&crate::types::Point],
805 ) -> Result<Option<Vec<crate::types::Point>>, SchemaError> {
806 if !points
809 .iter()
810 .any(|p| p.fields().iter().any(|(_, v)| v.is_decimal()))
811 {
812 return Ok(None);
813 }
814
815 let mut out: Vec<crate::types::Point> = Vec::with_capacity(points.len());
816 for point in points {
817 let mut rewritten: Option<crate::types::Point> = None;
818 let schema = self.schemas.get(point.series_key().measurement());
819 for (field_name, field_value) in point.fields() {
820 let FieldValue::Decimal(value) = field_value else {
821 continue;
822 };
823 let Some(scale) = schema
824 .as_ref()
825 .and_then(|s| s.column(field_name.as_ref()))
826 .and_then(|c| c.column_type.decimal_scale())
827 else {
828 continue;
829 };
830 if scale == value.scale() {
831 continue;
832 }
833 let rescaled = value.rescale(scale).map_err(|_| {
834 MeasurementSchema::mismatch(
839 point.series_key().measurement(),
840 field_name.as_ref(),
841 ColumnType::Decimal { scale },
842 field_value,
843 )
844 })?;
845 rewritten
846 .get_or_insert_with(|| (*point).clone())
847 .set_field(field_name.as_ref(), FieldValue::Decimal(rescaled));
848 }
849 out.push(rewritten.unwrap_or_else(|| (*point).clone()));
850 }
851 Ok(Some(out))
852 }
853
854 pub fn declare_field(
870 &self,
871 measurement: &str,
872 field: &str,
873 column_type: ColumnType,
874 ) -> Result<Vec<SchemaAction>, SchemaError> {
875 let _guard = self
876 .changes
877 .lock()
878 .unwrap_or_else(std::sync::PoisonError::into_inner);
879
880 let mut actions = Vec::new();
881 let mut schema = match self.schemas.get(measurement) {
882 Some(existing) => (**existing).clone(),
883 None => {
884 let fresh = MeasurementSchema::new(measurement);
885 actions.push(SchemaAction::CreateMeasurement(fresh.clone()));
886 fresh
887 }
888 };
889 if schema.declare_field(field, column_type)? {
890 actions.push(SchemaAction::AddColumn {
891 measurement: measurement.to_string(),
892 column: ColumnDef {
893 name: field.to_string(),
894 column_type,
895 role: ColumnRole::Field,
896 },
897 });
898 } else if actions.is_empty() {
899 return Ok(Vec::new());
901 }
902 self.schemas
903 .insert(measurement.to_string(), Arc::new(schema));
904 Ok(actions)
905 }
906
907 fn point_fits(
910 schema: &MeasurementSchema,
911 point: &crate::types::Point,
912 ) -> Result<bool, SchemaError> {
913 for tag_key in point.series_key().tag_keys() {
914 if schema.column(tag_key).is_none() {
915 return Ok(false);
916 }
917 }
918 for (field_name, field_value) in point.fields() {
919 match schema.column(field_name.as_ref()) {
920 Some(existing) => {
921 if !existing.column_type.accepts_value(field_value) {
922 return Err(MeasurementSchema::mismatch(
923 schema.measurement(),
924 field_name.as_ref(),
925 existing.column_type,
926 field_value,
927 ));
928 }
929 }
930 None => return Ok(false),
931 }
932 }
933 Ok(true)
934 }
935
936 pub fn register_point(
944 &self,
945 point: &crate::types::Point,
946 ) -> Result<Vec<SchemaAction>, SchemaError> {
947 self.register_batch(&[point])
948 }
949
950 #[must_use]
954 pub fn lookup(&self, measurement: &str) -> Option<Arc<MeasurementSchema>> {
955 self.schemas.get(measurement).map(|r| r.value().clone())
956 }
957
958 pub fn register_schema(&self, name: &str, schema: MeasurementSchema) {
962 self.schemas
963 .entry(name.to_string())
964 .or_insert(Arc::new(schema));
965 }
966
967 pub fn register_measurement(&self, ms: MeasurementSchema) {
971 self.schemas
972 .entry(ms.measurement().to_string())
973 .or_insert(Arc::new(ms));
974 }
975
976 pub fn apply_add_column(&self, measurement: &str, column: ColumnDef) {
981 if let Some(mut schema_arc) = self.schemas.get_mut(measurement) {
982 if let Some(existing) = schema_arc
983 .value()
984 .columns()
985 .iter()
986 .find(|c| c.name == column.name)
987 {
988 if existing.role != column.role {
990 tracing::warn!(
991 measurement,
992 column = %column.name,
993 existing_role = ?existing.role,
994 new_role = ?column.role,
995 "WAL replay column role conflict — skipping"
996 );
997 }
998 return;
1000 }
1001 let schema = Arc::make_mut(schema_arc.value_mut());
1002 match column.role {
1003 ColumnRole::Tag => {
1004 let _ = schema.add_tag(&column.name);
1005 }
1006 ColumnRole::Field => {
1007 schema.push_column(column);
1010 }
1011 ColumnRole::Timestamp => {
1012 }
1014 }
1015 }
1016 }
1017
1018 #[must_use]
1020 pub fn measurement_count(&self) -> usize {
1021 self.schemas.len()
1022 }
1023
1024 #[must_use]
1026 pub fn measurement_names(&self) -> Vec<String> {
1027 self.schemas.iter().map(|r| r.key().clone()).collect()
1028 }
1029
1030 #[must_use]
1032 pub fn remove(&self, measurement: &str) -> Option<Arc<MeasurementSchema>> {
1033 self.schemas.remove(measurement).map(|(_, v)| v)
1034 }
1035}
1036
1037impl Default for SchemaRegistry {
1038 fn default() -> Self {
1039 Self::new()
1040 }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use super::*;
1046 use crate::types::{FieldValue, Point, SeriesKey};
1047 use std::collections::BTreeMap;
1048
1049 fn make_point(
1050 measurement: &str,
1051 tags: &[(&str, &str)],
1052 fields: &[(&str, FieldValue)],
1053 ts: i64,
1054 ) -> Point {
1055 let tag_map: BTreeMap<String, String> = tags
1056 .iter()
1057 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1058 .collect();
1059 let field_map: BTreeMap<String, FieldValue> = fields
1060 .iter()
1061 .map(|(k, v)| ((*k).to_string(), v.clone()))
1062 .collect();
1063 let key = SeriesKey::new(measurement, tag_map).unwrap();
1064 Point::new(key, field_map, ts).unwrap()
1065 }
1066
1067 #[test]
1068 fn measurement_schema_new() {
1069 let schema = MeasurementSchema::new("cpu");
1070 assert_eq!(schema.measurement(), "cpu");
1071 assert_eq!(schema.columns().len(), 1);
1072 assert_eq!(schema.columns()[0].name, crate::TIME_COLUMN);
1073 assert_eq!(schema.columns()[0].role, ColumnRole::Timestamp);
1074 }
1075
1076 #[test]
1077 fn measurement_schema_add_tag() {
1078 let mut schema = MeasurementSchema::new("cpu");
1079 assert!(schema.add_tag("host"));
1080 assert!(!schema.add_tag("host")); assert_eq!(schema.tag_count(), 1);
1082 assert_eq!(schema.columns()[1].name, "host");
1083 assert_eq!(schema.columns()[1].role, ColumnRole::Tag);
1084 }
1085
1086 #[test]
1087 fn measurement_schema_add_field() {
1088 let mut schema = MeasurementSchema::new("cpu");
1089 let added = schema.add_field("value", &FieldValue::F64(1.0)).unwrap();
1090 assert!(added);
1091 let added = schema.add_field("value", &FieldValue::F64(2.0)).unwrap();
1092 assert!(!added); assert_eq!(schema.field_count(), 1);
1094 }
1095
1096 #[test]
1097 fn measurement_schema_type_conflict() {
1098 let mut schema = MeasurementSchema::new("cpu");
1099 schema.add_field("value", &FieldValue::F64(1.0)).unwrap();
1100 let err = schema.add_field("value", &FieldValue::I64(1)).unwrap_err();
1101 assert!(matches!(err, SchemaError::TypeConflict { .. }));
1102 }
1103
1104 #[test]
1105 fn schema_registry_auto_create() {
1106 let registry = SchemaRegistry::new();
1107 let point = make_point(
1108 "cpu",
1109 &[("host", "srv1")],
1110 &[("value", FieldValue::F64(72.5))],
1111 1_000_000_000,
1112 );
1113 let actions = registry.register_point(&point).unwrap();
1114 assert_eq!(actions.len(), 3);
1116 assert!(matches!(&actions[0], SchemaAction::CreateMeasurement(_)));
1117
1118 let schema = registry.lookup("cpu").unwrap();
1119 assert_eq!(schema.measurement(), "cpu");
1120 assert_eq!(schema.tag_count(), 1);
1121 assert_eq!(schema.field_count(), 1);
1122 }
1123
1124 #[test]
1125 fn schema_registry_additive_evolution() {
1126 let registry = SchemaRegistry::new();
1127
1128 let p1 = make_point(
1130 "cpu",
1131 &[("host", "srv1")],
1132 &[("value", FieldValue::F64(72.5))],
1133 1,
1134 );
1135 registry.register_point(&p1).unwrap();
1136
1137 let p2 = make_point(
1139 "cpu",
1140 &[("host", "srv1"), ("dc", "eu")],
1141 &[
1142 ("value", FieldValue::F64(73.0)),
1143 ("cores", FieldValue::I64(8)),
1144 ],
1145 2,
1146 );
1147 let actions = registry.register_point(&p2).unwrap();
1148 assert_eq!(actions.len(), 2);
1150
1151 let schema = registry.lookup("cpu").unwrap();
1152 assert_eq!(schema.tag_count(), 2);
1153 assert_eq!(schema.field_count(), 2);
1154 }
1155
1156 #[test]
1157 fn schema_registry_type_conflict_rejected() {
1158 let registry = SchemaRegistry::new();
1159
1160 let p1 = make_point("cpu", &[], &[("value", FieldValue::F64(72.5))], 1);
1161 registry.register_point(&p1).unwrap();
1162
1163 let p2 = make_point("cpu", &[], &[("value", FieldValue::I64(72))], 2);
1164 let err = registry.register_point(&p2).unwrap_err();
1165 assert!(matches!(err, SchemaError::TypeConflict { .. }));
1166 }
1167
1168 #[test]
1169 fn schema_registry_multiple_measurements() {
1170 let registry = SchemaRegistry::new();
1171
1172 let p1 = make_point("cpu", &[], &[("value", FieldValue::F64(72.5))], 1);
1173 let p2 = make_point("mem", &[], &[("used", FieldValue::U64(1024))], 1);
1174
1175 registry.register_point(&p1).unwrap();
1176 registry.register_point(&p2).unwrap();
1177
1178 assert_eq!(registry.measurement_count(), 2);
1179 assert!(registry.lookup("cpu").is_some());
1180 assert!(registry.lookup("mem").is_some());
1181 assert!(registry.lookup("disk").is_none());
1182 }
1183
1184 #[test]
1185 fn schema_registry_remove() {
1186 let registry = SchemaRegistry::new();
1187 let p = make_point("cpu", &[], &[("value", FieldValue::F64(1.0))], 1);
1188 registry.register_point(&p).unwrap();
1189 assert!(registry.lookup("cpu").is_some());
1190
1191 let removed = registry.remove("cpu");
1192 assert!(removed.is_some());
1193 assert!(registry.lookup("cpu").is_none());
1194 assert_eq!(registry.measurement_count(), 0);
1195 }
1196
1197 #[test]
1198 fn schema_registry_measurement_names() {
1199 let registry = SchemaRegistry::new();
1200 let p1 = make_point("cpu", &[], &[("v", FieldValue::F64(1.0))], 1);
1201 let p2 = make_point("mem", &[], &[("v", FieldValue::U64(1))], 1);
1202 registry.register_point(&p1).unwrap();
1203 registry.register_point(&p2).unwrap();
1204
1205 let mut names = registry.measurement_names();
1206 names.sort();
1207 assert_eq!(names, vec!["cpu", "mem"]);
1208 }
1209
1210 #[test]
1211 fn schema_serde_roundtrip() {
1212 let mut schema = MeasurementSchema::new("cpu");
1213 let _ = schema.add_tag("host");
1214 schema.add_field("value", &FieldValue::F64(1.0)).unwrap();
1215
1216 let json = serde_json::to_string(&schema).unwrap();
1217 let back: MeasurementSchema = serde_json::from_str(&json).unwrap();
1218 assert_eq!(schema, back);
1219 }
1220
1221 #[test]
1222 fn a_decimal_column_accepts_a_narrower_value_and_refuses_a_finer_one() {
1223 let mut schema = MeasurementSchema::new("meter");
1224 schema
1225 .declare_field("z1nb", ColumnType::Decimal { scale: 4 })
1226 .unwrap();
1227 assert!(
1229 !schema
1230 .add_field("z1nb", &FieldValue::Decimal("1.5".parse().unwrap()))
1231 .unwrap()
1232 );
1233 assert!(
1235 !schema
1236 .add_field("z1nb", &FieldValue::Decimal("1.50000".parse().unwrap()))
1237 .unwrap()
1238 );
1239 let err = schema
1241 .add_field("z1nb", &FieldValue::Decimal("1.00005".parse().unwrap()))
1242 .unwrap_err();
1243 assert!(
1244 matches!(
1245 err,
1246 SchemaError::DecimalScaleConflict {
1247 declared: 4,
1248 got: 5,
1249 ..
1250 }
1251 ),
1252 "{err:?}"
1253 );
1254 }
1255
1256 #[test]
1257 fn running_out_of_digits_is_not_reported_as_a_scale_conflict() {
1258 let mut schema = MeasurementSchema::new("m");
1262 schema
1263 .declare_field("v", ColumnType::Decimal { scale: 4 })
1264 .unwrap();
1265 let huge = crate::decimal::Decimal::new(10_i128.pow(37), 0).unwrap();
1266 let err = schema
1267 .add_field("v", &FieldValue::Decimal(huge))
1268 .unwrap_err();
1269 match err {
1270 SchemaError::InvalidFieldValue { ref reason, .. } => {
1271 assert!(reason.contains("significant digits"), "{reason}");
1272 }
1273 other => panic!("expected a precision error, got {other:?}"),
1274 }
1275 }
1276
1277 #[test]
1278 fn a_decimal_and_another_type_are_still_a_type_conflict() {
1279 let mut schema = MeasurementSchema::new("m");
1280 schema
1281 .declare_field("v", ColumnType::Decimal { scale: 2 })
1282 .unwrap();
1283 let err = schema.add_field("v", &FieldValue::F64(1.5)).unwrap_err();
1284 assert!(matches!(err, SchemaError::TypeConflict { .. }), "{err:?}");
1285 }
1286
1287 #[test]
1288 fn a_batch_creating_a_decimal_column_takes_its_widest_scale() {
1289 let registry = SchemaRegistry::new();
1290 for order in [["1.5", "1.4999"], ["1.4999", "1.5"]] {
1292 let registry = registry.clone();
1293 let points: Vec<Point> = order
1294 .iter()
1295 .enumerate()
1296 .map(|(i, text)| {
1297 make_point(
1298 "meter",
1299 &[],
1300 &[("v", FieldValue::Decimal(text.parse().unwrap()))],
1301 i as i64,
1302 )
1303 })
1304 .collect();
1305 let refs: Vec<&Point> = points.iter().collect();
1306 registry.register_batch(&refs).unwrap();
1307 assert_eq!(
1308 registry
1309 .lookup("meter")
1310 .unwrap()
1311 .column("v")
1312 .unwrap()
1313 .column_type,
1314 ColumnType::Decimal { scale: 4 },
1315 "order {order:?}"
1316 );
1317 }
1318 }
1319
1320 #[test]
1321 fn normalize_decimals_widens_to_the_column_and_copies_nothing_otherwise() {
1322 let registry = SchemaRegistry::new();
1323 let point = make_point(
1324 "meter",
1325 &[],
1326 &[("v", FieldValue::Decimal("1.5000".parse().unwrap()))],
1327 1,
1328 );
1329 registry.register_batch(&[&point]).unwrap();
1330
1331 let plain = make_point("cpu", &[], &[("v", FieldValue::F64(1.0))], 1);
1333 assert!(registry.normalize_decimals(&[&plain]).unwrap().is_none());
1334
1335 let narrow = make_point(
1337 "meter",
1338 &[],
1339 &[("v", FieldValue::Decimal("1.5".parse().unwrap()))],
1340 2,
1341 );
1342 let out = registry.normalize_decimals(&[&narrow]).unwrap().unwrap();
1343 match out[0].field("v") {
1344 Some(FieldValue::Decimal(d)) => {
1345 assert_eq!(d.scale(), 4);
1346 assert_eq!(d.to_string(), "1.5000");
1347 }
1348 other => panic!("expected a decimal, got {other:?}"),
1349 }
1350 }
1351
1352 #[test]
1353 fn column_type_from_field_value() {
1354 assert_eq!(
1355 ColumnType::from_field_value(&FieldValue::F64(1.0)),
1356 ColumnType::F64
1357 );
1358 assert_eq!(
1359 ColumnType::from_field_value(&FieldValue::I64(1)),
1360 ColumnType::I64
1361 );
1362 assert_eq!(
1363 ColumnType::from_field_value(&FieldValue::U64(1)),
1364 ColumnType::U64
1365 );
1366 assert_eq!(
1367 ColumnType::from_field_value(&FieldValue::Bool(true)),
1368 ColumnType::Bool
1369 );
1370 assert_eq!(
1371 ColumnType::from_field_value(&FieldValue::String("x".into())),
1372 ColumnType::String
1373 );
1374 }
1375
1376 #[test]
1377 fn column_type_display() {
1378 assert_eq!(ColumnType::Timestamp.to_string(), "timestamp");
1379 assert_eq!(ColumnType::String.to_string(), "string");
1380 assert_eq!(ColumnType::F64.to_string(), "f64");
1381 assert_eq!(ColumnType::I64.to_string(), "i64");
1382 assert_eq!(ColumnType::U64.to_string(), "u64");
1383 assert_eq!(ColumnType::Bool.to_string(), "bool");
1384 }
1385
1386 #[test]
1387 fn column_role_display() {
1388 assert_eq!(ColumnRole::Timestamp.to_string(), "timestamp");
1389 assert_eq!(ColumnRole::Tag.to_string(), "tag");
1390 assert_eq!(ColumnRole::Field.to_string(), "field");
1391 }
1392
1393 #[test]
1394 fn measurement_schema_column_not_found() {
1395 let schema = MeasurementSchema::new("cpu");
1396 assert!(schema.column("nonexistent").is_none());
1397 }
1398
1399 #[test]
1400 fn schema_registry_remove_nonexistent() {
1401 let registry = SchemaRegistry::new();
1402 let removed = registry.remove("nope");
1403 assert!(removed.is_none());
1404 }
1405
1406 #[test]
1407 fn schema_action_serde_roundtrip() {
1408 let action = SchemaAction::CreateMeasurement(MeasurementSchema::new("cpu"));
1409 let json = serde_json::to_string(&action).unwrap();
1410 let back: SchemaAction = serde_json::from_str(&json).unwrap();
1411 assert_eq!(action, back);
1412
1413 let action = SchemaAction::AddColumn {
1414 measurement: "cpu".into(),
1415 column: ColumnDef {
1416 name: "value".into(),
1417 column_type: ColumnType::F64,
1418 role: ColumnRole::Field,
1419 },
1420 };
1421 let json = serde_json::to_string(&action).unwrap();
1422 let back: SchemaAction = serde_json::from_str(&json).unwrap();
1423 assert_eq!(action, back);
1424 }
1425
1426 #[test]
1427 fn measurement_schema_accessors() {
1428 let mut schema = MeasurementSchema::new("cpu");
1429 assert!(schema.add_tag("host"));
1430 assert_eq!(schema.measurement(), "cpu");
1431 assert_eq!(schema.columns().len(), 2);
1432 }
1433}