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;
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}
81
82impl ColumnType {
83 #[inline]
85 #[must_use]
86 pub fn from_field_value(value: &FieldValue) -> Self {
87 match value {
88 FieldValue::F64(_) => Self::F64,
89 FieldValue::I64(_) => Self::I64,
90 FieldValue::U64(_) => Self::U64,
91 FieldValue::Bool(_) => Self::Bool,
92 FieldValue::String(_) => Self::String,
93 }
94 }
95}
96
97impl fmt::Display for ColumnType {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 Self::Timestamp => write!(f, "timestamp"),
101 Self::String => write!(f, "string"),
102 Self::F64 => write!(f, "f64"),
103 Self::I64 => write!(f, "i64"),
104 Self::U64 => write!(f, "u64"),
105 Self::Bool => write!(f, "bool"),
106 }
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct ColumnDef {
113 pub name: String,
115 pub column_type: ColumnType,
117 pub role: ColumnRole,
119}
120
121#[derive(Debug, Clone, Serialize)]
128pub struct MeasurementSchema {
129 measurement: String,
130 columns: Vec<ColumnDef>,
131 #[serde(skip)]
133 column_index: HashMap<String, usize>,
134}
135
136impl PartialEq for MeasurementSchema {
137 fn eq(&self, other: &Self) -> bool {
138 self.measurement == other.measurement && self.columns == other.columns
139 }
140}
141
142impl Eq for MeasurementSchema {}
143
144impl<'de> serde::Deserialize<'de> for MeasurementSchema {
145 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
146 where
147 D: serde::Deserializer<'de>,
148 {
149 #[derive(Deserialize)]
150 struct Helper {
151 measurement: String,
152 columns: Vec<ColumnDef>,
153 }
154 let h = Helper::deserialize(deserializer)?;
155 let column_index = h
156 .columns
157 .iter()
158 .enumerate()
159 .map(|(i, c)| (c.name.clone(), i))
160 .collect();
161 Ok(Self {
162 measurement: h.measurement,
163 columns: h.columns,
164 column_index,
165 })
166 }
167}
168
169impl MeasurementSchema {
170 #[must_use]
172 pub fn new(measurement: impl Into<String>) -> Self {
173 let ts_col = ColumnDef {
174 name: crate::TIME_COLUMN.to_string(),
175 column_type: ColumnType::Timestamp,
176 role: ColumnRole::Timestamp,
177 };
178 let mut column_index = HashMap::new();
179 column_index.insert(ts_col.name.clone(), 0);
180 Self {
181 measurement: measurement.into(),
182 columns: vec![ts_col],
183 column_index,
184 }
185 }
186
187 #[inline]
189 #[must_use]
190 pub fn measurement(&self) -> &str {
191 &self.measurement
192 }
193
194 #[inline]
196 #[must_use]
197 pub fn columns(&self) -> &[ColumnDef] {
198 &self.columns
199 }
200
201 #[must_use]
207 pub fn add_tag(&mut self, name: &str) -> bool {
208 if let Some(existing) = self.columns.iter().find(|c| c.name == name) {
209 if existing.role != ColumnRole::Tag {
210 tracing::error!(
213 measurement = %self.measurement,
214 column = name,
215 existing_role = ?existing.role,
216 "tag name conflicts with existing column role — rejecting"
217 );
218 }
219 return false;
220 }
221 let idx = self.columns.len();
222 self.columns.push(ColumnDef {
223 name: name.to_string(),
224 column_type: ColumnType::String,
225 role: ColumnRole::Tag,
226 });
227 self.column_index.insert(name.to_string(), idx);
228 true
229 }
230
231 pub fn add_field(&mut self, name: &str, field_value: &FieldValue) -> Result<bool, SchemaError> {
238 let new_type = ColumnType::from_field_value(field_value);
239
240 if let Some(existing) = self.columns.iter().find(|c| c.name == name) {
241 if existing.role != ColumnRole::Field {
243 return Err(SchemaError::TypeConflict {
244 measurement: self.measurement.clone(),
245 field: name.to_string(),
246 expected: format!("{:?}", existing.role),
247 got: "Field".to_string(),
248 });
249 }
250 if existing.column_type != new_type {
251 return Err(SchemaError::TypeConflict {
252 measurement: self.measurement.clone(),
253 field: name.to_string(),
254 expected: existing.column_type.to_string(),
255 got: new_type.to_string(),
256 });
257 }
258 return Ok(false);
259 }
260
261 let idx = self.columns.len();
262 self.columns.push(ColumnDef {
263 name: name.to_string(),
264 column_type: new_type,
265 role: ColumnRole::Field,
266 });
267 self.column_index.insert(name.to_string(), idx);
268 Ok(true)
269 }
270
271 #[inline]
273 #[must_use]
274 pub fn column(&self, name: &str) -> Option<&ColumnDef> {
275 self.column_index
276 .get(name)
277 .and_then(|&i| self.columns.get(i))
278 }
279
280 pub fn push_column(&mut self, col: ColumnDef) {
284 if self.column_index.contains_key(&col.name) {
285 return; }
287 let idx = self.columns.len();
288 self.column_index.insert(col.name.clone(), idx);
289 self.columns.push(col);
290 }
291
292 #[must_use]
294 pub fn tag_count(&self) -> usize {
295 self.columns
296 .iter()
297 .filter(|c| c.role == ColumnRole::Tag)
298 .count()
299 }
300
301 #[must_use]
303 pub fn field_count(&self) -> usize {
304 self.columns
305 .iter()
306 .filter(|c| c.role == ColumnRole::Field)
307 .count()
308 }
309
310 #[must_use]
312 pub fn tag_names(&self) -> Vec<&str> {
313 self.columns
314 .iter()
315 .filter(|c| c.role == ColumnRole::Tag)
316 .map(|c| c.name.as_str())
317 .collect()
318 }
319
320 #[must_use]
322 pub fn field_names(&self) -> Vec<&str> {
323 self.columns
324 .iter()
325 .filter(|c| c.role == ColumnRole::Field)
326 .map(|c| c.name.as_str())
327 .collect()
328 }
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
333pub enum SchemaAction {
334 CreateMeasurement(MeasurementSchema),
336 AddColumn {
338 measurement: String,
340 column: ColumnDef,
342 },
343}
344
345#[derive(Debug, Clone)]
366pub struct SchemaRegistry {
367 schemas: Arc<DashMap<String, Arc<MeasurementSchema>>>,
370 changes: Arc<std::sync::Mutex<()>>,
379}
380
381impl SchemaRegistry {
382 #[must_use]
384 pub fn new() -> Self {
385 Self {
386 schemas: Arc::new(DashMap::new()),
387 changes: Arc::new(std::sync::Mutex::new(())),
388 }
389 }
390
391 pub fn register_batch(
411 &self,
412 points: &[&crate::types::Point],
413 ) -> Result<Vec<SchemaAction>, SchemaError> {
414 let mut all_known = true;
416 for point in points {
417 match self.schemas.get(point.series_key().measurement()) {
418 Some(schema) => {
419 if !Self::point_fits(&schema, point)? {
420 all_known = false;
421 }
422 }
423 None => all_known = false,
424 }
425 }
426 if all_known {
427 return Ok(Vec::new());
428 }
429
430 let _guard = self
433 .changes
434 .lock()
435 .unwrap_or_else(std::sync::PoisonError::into_inner);
436 let mut pending: std::collections::BTreeMap<String, MeasurementSchema> =
437 std::collections::BTreeMap::new();
438 let mut actions = Vec::new();
439
440 for point in points {
441 let name = point.series_key().measurement();
442 let schema = match pending.get_mut(name) {
443 Some(s) => s,
444 None => {
445 let fresh = match self.schemas.get(name) {
446 Some(existing) => (**existing).clone(),
447 None => {
448 let s = MeasurementSchema::new(name);
449 actions.push(SchemaAction::CreateMeasurement(s.clone()));
450 s
451 }
452 };
453 pending.entry(name.to_string()).or_insert(fresh)
454 }
455 };
456 for tag_key in point.series_key().tag_keys() {
457 if schema.add_tag(tag_key) {
458 actions.push(SchemaAction::AddColumn {
459 measurement: name.to_string(),
460 column: ColumnDef {
461 name: tag_key.to_string(),
462 column_type: ColumnType::String,
463 role: ColumnRole::Tag,
464 },
465 });
466 }
467 }
468 for (field_name, field_value) in point.fields() {
469 if schema.add_field(field_name.as_ref(), field_value)? {
470 actions.push(SchemaAction::AddColumn {
471 measurement: name.to_string(),
472 column: ColumnDef {
473 name: field_name.to_string(),
474 column_type: ColumnType::from_field_value(field_value),
475 role: ColumnRole::Field,
476 },
477 });
478 }
479 }
480 }
481
482 for (name, schema) in pending {
483 self.schemas.insert(name, Arc::new(schema));
484 }
485 Ok(actions)
486 }
487
488 fn point_fits(
491 schema: &MeasurementSchema,
492 point: &crate::types::Point,
493 ) -> Result<bool, SchemaError> {
494 for tag_key in point.series_key().tag_keys() {
495 if schema.column(tag_key).is_none() {
496 return Ok(false);
497 }
498 }
499 for (field_name, field_value) in point.fields() {
500 match schema.column(field_name.as_ref()) {
501 Some(existing) => {
502 let new_type = ColumnType::from_field_value(field_value);
503 if existing.column_type != new_type {
504 return Err(SchemaError::TypeConflict {
505 measurement: schema.measurement().to_string(),
506 field: field_name.to_string(),
507 expected: existing.column_type.to_string(),
508 got: new_type.to_string(),
509 });
510 }
511 }
512 None => return Ok(false),
513 }
514 }
515 Ok(true)
516 }
517
518 pub fn register_point(
526 &self,
527 point: &crate::types::Point,
528 ) -> Result<Vec<SchemaAction>, SchemaError> {
529 self.register_batch(&[point])
530 }
531
532 #[must_use]
536 pub fn lookup(&self, measurement: &str) -> Option<Arc<MeasurementSchema>> {
537 self.schemas.get(measurement).map(|r| r.value().clone())
538 }
539
540 pub fn register_schema(&self, name: &str, schema: MeasurementSchema) {
544 self.schemas
545 .entry(name.to_string())
546 .or_insert(Arc::new(schema));
547 }
548
549 pub fn register_measurement(&self, ms: MeasurementSchema) {
553 self.schemas
554 .entry(ms.measurement().to_string())
555 .or_insert(Arc::new(ms));
556 }
557
558 pub fn apply_add_column(&self, measurement: &str, column: ColumnDef) {
563 if let Some(mut schema_arc) = self.schemas.get_mut(measurement) {
564 if let Some(existing) = schema_arc
565 .value()
566 .columns()
567 .iter()
568 .find(|c| c.name == column.name)
569 {
570 if existing.role != column.role {
572 tracing::warn!(
573 measurement,
574 column = %column.name,
575 existing_role = ?existing.role,
576 new_role = ?column.role,
577 "WAL replay column role conflict — skipping"
578 );
579 }
580 return;
582 }
583 let schema = Arc::make_mut(schema_arc.value_mut());
584 match column.role {
585 ColumnRole::Tag => {
586 let _ = schema.add_tag(&column.name);
587 }
588 ColumnRole::Field => {
589 schema.push_column(column);
592 }
593 ColumnRole::Timestamp => {
594 }
596 }
597 }
598 }
599
600 #[must_use]
602 pub fn measurement_count(&self) -> usize {
603 self.schemas.len()
604 }
605
606 #[must_use]
608 pub fn measurement_names(&self) -> Vec<String> {
609 self.schemas.iter().map(|r| r.key().clone()).collect()
610 }
611
612 #[must_use]
614 pub fn remove(&self, measurement: &str) -> Option<Arc<MeasurementSchema>> {
615 self.schemas.remove(measurement).map(|(_, v)| v)
616 }
617}
618
619impl Default for SchemaRegistry {
620 fn default() -> Self {
621 Self::new()
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use crate::types::{FieldValue, Point, SeriesKey};
629 use std::collections::BTreeMap;
630
631 fn make_point(
632 measurement: &str,
633 tags: &[(&str, &str)],
634 fields: &[(&str, FieldValue)],
635 ts: i64,
636 ) -> Point {
637 let tag_map: BTreeMap<String, String> = tags
638 .iter()
639 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
640 .collect();
641 let field_map: BTreeMap<String, FieldValue> = fields
642 .iter()
643 .map(|(k, v)| ((*k).to_string(), v.clone()))
644 .collect();
645 let key = SeriesKey::new(measurement, tag_map).unwrap();
646 Point::new(key, field_map, ts).unwrap()
647 }
648
649 #[test]
650 fn measurement_schema_new() {
651 let schema = MeasurementSchema::new("cpu");
652 assert_eq!(schema.measurement(), "cpu");
653 assert_eq!(schema.columns().len(), 1);
654 assert_eq!(schema.columns()[0].name, crate::TIME_COLUMN);
655 assert_eq!(schema.columns()[0].role, ColumnRole::Timestamp);
656 }
657
658 #[test]
659 fn measurement_schema_add_tag() {
660 let mut schema = MeasurementSchema::new("cpu");
661 assert!(schema.add_tag("host"));
662 assert!(!schema.add_tag("host")); assert_eq!(schema.tag_count(), 1);
664 assert_eq!(schema.columns()[1].name, "host");
665 assert_eq!(schema.columns()[1].role, ColumnRole::Tag);
666 }
667
668 #[test]
669 fn measurement_schema_add_field() {
670 let mut schema = MeasurementSchema::new("cpu");
671 let added = schema.add_field("value", &FieldValue::F64(1.0)).unwrap();
672 assert!(added);
673 let added = schema.add_field("value", &FieldValue::F64(2.0)).unwrap();
674 assert!(!added); assert_eq!(schema.field_count(), 1);
676 }
677
678 #[test]
679 fn measurement_schema_type_conflict() {
680 let mut schema = MeasurementSchema::new("cpu");
681 schema.add_field("value", &FieldValue::F64(1.0)).unwrap();
682 let err = schema.add_field("value", &FieldValue::I64(1)).unwrap_err();
683 assert!(matches!(err, SchemaError::TypeConflict { .. }));
684 }
685
686 #[test]
687 fn schema_registry_auto_create() {
688 let registry = SchemaRegistry::new();
689 let point = make_point(
690 "cpu",
691 &[("host", "srv1")],
692 &[("value", FieldValue::F64(72.5))],
693 1_000_000_000,
694 );
695 let actions = registry.register_point(&point).unwrap();
696 assert_eq!(actions.len(), 3);
698 assert!(matches!(&actions[0], SchemaAction::CreateMeasurement(_)));
699
700 let schema = registry.lookup("cpu").unwrap();
701 assert_eq!(schema.measurement(), "cpu");
702 assert_eq!(schema.tag_count(), 1);
703 assert_eq!(schema.field_count(), 1);
704 }
705
706 #[test]
707 fn schema_registry_additive_evolution() {
708 let registry = SchemaRegistry::new();
709
710 let p1 = make_point(
712 "cpu",
713 &[("host", "srv1")],
714 &[("value", FieldValue::F64(72.5))],
715 1,
716 );
717 registry.register_point(&p1).unwrap();
718
719 let p2 = make_point(
721 "cpu",
722 &[("host", "srv1"), ("dc", "eu")],
723 &[
724 ("value", FieldValue::F64(73.0)),
725 ("cores", FieldValue::I64(8)),
726 ],
727 2,
728 );
729 let actions = registry.register_point(&p2).unwrap();
730 assert_eq!(actions.len(), 2);
732
733 let schema = registry.lookup("cpu").unwrap();
734 assert_eq!(schema.tag_count(), 2);
735 assert_eq!(schema.field_count(), 2);
736 }
737
738 #[test]
739 fn schema_registry_type_conflict_rejected() {
740 let registry = SchemaRegistry::new();
741
742 let p1 = make_point("cpu", &[], &[("value", FieldValue::F64(72.5))], 1);
743 registry.register_point(&p1).unwrap();
744
745 let p2 = make_point("cpu", &[], &[("value", FieldValue::I64(72))], 2);
746 let err = registry.register_point(&p2).unwrap_err();
747 assert!(matches!(err, SchemaError::TypeConflict { .. }));
748 }
749
750 #[test]
751 fn schema_registry_multiple_measurements() {
752 let registry = SchemaRegistry::new();
753
754 let p1 = make_point("cpu", &[], &[("value", FieldValue::F64(72.5))], 1);
755 let p2 = make_point("mem", &[], &[("used", FieldValue::U64(1024))], 1);
756
757 registry.register_point(&p1).unwrap();
758 registry.register_point(&p2).unwrap();
759
760 assert_eq!(registry.measurement_count(), 2);
761 assert!(registry.lookup("cpu").is_some());
762 assert!(registry.lookup("mem").is_some());
763 assert!(registry.lookup("disk").is_none());
764 }
765
766 #[test]
767 fn schema_registry_remove() {
768 let registry = SchemaRegistry::new();
769 let p = make_point("cpu", &[], &[("value", FieldValue::F64(1.0))], 1);
770 registry.register_point(&p).unwrap();
771 assert!(registry.lookup("cpu").is_some());
772
773 let removed = registry.remove("cpu");
774 assert!(removed.is_some());
775 assert!(registry.lookup("cpu").is_none());
776 assert_eq!(registry.measurement_count(), 0);
777 }
778
779 #[test]
780 fn schema_registry_measurement_names() {
781 let registry = SchemaRegistry::new();
782 let p1 = make_point("cpu", &[], &[("v", FieldValue::F64(1.0))], 1);
783 let p2 = make_point("mem", &[], &[("v", FieldValue::U64(1))], 1);
784 registry.register_point(&p1).unwrap();
785 registry.register_point(&p2).unwrap();
786
787 let mut names = registry.measurement_names();
788 names.sort();
789 assert_eq!(names, vec!["cpu", "mem"]);
790 }
791
792 #[test]
793 fn schema_serde_roundtrip() {
794 let mut schema = MeasurementSchema::new("cpu");
795 let _ = schema.add_tag("host");
796 schema.add_field("value", &FieldValue::F64(1.0)).unwrap();
797
798 let json = serde_json::to_string(&schema).unwrap();
799 let back: MeasurementSchema = serde_json::from_str(&json).unwrap();
800 assert_eq!(schema, back);
801 }
802
803 #[test]
804 fn column_type_from_field_value() {
805 assert_eq!(
806 ColumnType::from_field_value(&FieldValue::F64(1.0)),
807 ColumnType::F64
808 );
809 assert_eq!(
810 ColumnType::from_field_value(&FieldValue::I64(1)),
811 ColumnType::I64
812 );
813 assert_eq!(
814 ColumnType::from_field_value(&FieldValue::U64(1)),
815 ColumnType::U64
816 );
817 assert_eq!(
818 ColumnType::from_field_value(&FieldValue::Bool(true)),
819 ColumnType::Bool
820 );
821 assert_eq!(
822 ColumnType::from_field_value(&FieldValue::String("x".into())),
823 ColumnType::String
824 );
825 }
826
827 #[test]
828 fn column_type_display() {
829 assert_eq!(ColumnType::Timestamp.to_string(), "timestamp");
830 assert_eq!(ColumnType::String.to_string(), "string");
831 assert_eq!(ColumnType::F64.to_string(), "f64");
832 assert_eq!(ColumnType::I64.to_string(), "i64");
833 assert_eq!(ColumnType::U64.to_string(), "u64");
834 assert_eq!(ColumnType::Bool.to_string(), "bool");
835 }
836
837 #[test]
838 fn column_role_display() {
839 assert_eq!(ColumnRole::Timestamp.to_string(), "timestamp");
840 assert_eq!(ColumnRole::Tag.to_string(), "tag");
841 assert_eq!(ColumnRole::Field.to_string(), "field");
842 }
843
844 #[test]
845 fn measurement_schema_column_not_found() {
846 let schema = MeasurementSchema::new("cpu");
847 assert!(schema.column("nonexistent").is_none());
848 }
849
850 #[test]
851 fn schema_registry_remove_nonexistent() {
852 let registry = SchemaRegistry::new();
853 let removed = registry.remove("nope");
854 assert!(removed.is_none());
855 }
856
857 #[test]
858 fn schema_action_serde_roundtrip() {
859 let action = SchemaAction::CreateMeasurement(MeasurementSchema::new("cpu"));
860 let json = serde_json::to_string(&action).unwrap();
861 let back: SchemaAction = serde_json::from_str(&json).unwrap();
862 assert_eq!(action, back);
863
864 let action = SchemaAction::AddColumn {
865 measurement: "cpu".into(),
866 column: ColumnDef {
867 name: "value".into(),
868 column_type: ColumnType::F64,
869 role: ColumnRole::Field,
870 },
871 };
872 let json = serde_json::to_string(&action).unwrap();
873 let back: SchemaAction = serde_json::from_str(&json).unwrap();
874 assert_eq!(action, back);
875 }
876
877 #[test]
878 fn measurement_schema_accessors() {
879 let mut schema = MeasurementSchema::new("cpu");
880 assert!(schema.add_tag("host"));
881 assert_eq!(schema.measurement(), "cpu");
882 assert_eq!(schema.columns().len(), 2);
883 }
884}