Skip to main content

kalamdb_commons/models/schemas/
schema_field.rs

1//! Schema field for API response serialization.
2
3#[cfg(feature = "schema-metadata")]
4use arrow_schema::Field;
5use serde::{Deserialize, Serialize};
6
7#[cfg(feature = "schema-metadata")]
8use crate::conversions::read_kalam_column_flags_metadata;
9use crate::{
10    models::datatypes::KalamDataType,
11    schemas::{ColumnDefinition, FieldFlag, FieldFlags},
12};
13
14/// A field in the result schema returned by SQL queries
15///
16/// Contains all the information a client needs to properly interpret
17/// column data, including the name, data type, and index.
18///
19/// # Example (JSON representation)
20///
21/// ```json
22/// {
23///   "name": "user_id",
24///   "data_type": "BigInt",
25///   "index": 0,
26///   "flags": ["pk", "nn", "uq"]
27/// }
28/// ```
29///
30/// # Example (with parameterized type)
31///
32/// ```json
33/// {
34///   "name": "vector",
35///   "data_type": { "Embedding": 384 },
36///   "index": 5
37/// }
38/// ```
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct SchemaField {
41    /// Column name
42    pub name: String,
43
44    /// Data type using KalamDB's unified type system
45    pub data_type: KalamDataType,
46
47    /// Column position (0-indexed) in the result set
48    pub index: usize,
49
50    /// Structured field flags (e.g. ["pk", "nn", "uq"]).
51    ///
52    /// Omitted when there are no notable flags to reduce response size.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub flags: Option<FieldFlags>,
55}
56
57impl SchemaField {
58    /// Create a new schema field
59    pub fn new(name: impl Into<String>, data_type: KalamDataType, index: usize) -> Self {
60        Self {
61            name: name.into(),
62            data_type,
63            index,
64            flags: None,
65        }
66    }
67
68    pub fn from_column_definition(column: &ColumnDefinition, index: usize) -> Self {
69        Self {
70            name: column.column_name.clone(),
71            data_type: column.data_type.clone(),
72            index,
73            flags: Self::flags_for_column(column.is_primary_key, column.is_nullable),
74        }
75    }
76
77    #[cfg(feature = "schema-metadata")]
78    pub fn from_arrow_field(field: &Field, data_type: KalamDataType, index: usize) -> Self {
79        let flags = read_kalam_column_flags_metadata(field);
80
81        Self {
82            name: field.name().clone(),
83            data_type,
84            index,
85            flags,
86        }
87    }
88
89    pub fn with_flags(mut self, flags: FieldFlags) -> Self {
90        if !flags.is_empty() {
91            self.flags = Some(flags);
92        }
93        self
94    }
95
96    pub fn flags_for_column(is_primary_key: bool, is_nullable: bool) -> Option<FieldFlags> {
97        let mut flags = FieldFlags::new();
98        if is_primary_key {
99            flags.insert(FieldFlag::PrimaryKey);
100            flags.insert(FieldFlag::Unique);
101        }
102        if !is_nullable {
103            flags.insert(FieldFlag::NonNull);
104        }
105
106        if flags.is_empty() {
107            None
108        } else {
109            Some(flags)
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use serde_json;
117
118    use super::*;
119
120    #[test]
121    fn test_schema_field_serialization() {
122        let field = SchemaField::new("user_id", KalamDataType::BigInt, 0);
123        let json = serde_json::to_string(&field).unwrap();
124        assert!(json.contains("\"name\":\"user_id\""));
125        assert!(json.contains("\"data_type\":\"BigInt\""));
126        assert!(json.contains("\"index\":0"));
127        assert!(!json.contains("\"flags\":"));
128    }
129
130    #[test]
131    fn test_schema_field_deserialization() {
132        let json = r#"{"name":"email","data_type":"Text","index":1}"#;
133        let field: SchemaField = serde_json::from_str(json).unwrap();
134        assert_eq!(field.name, "email");
135        assert_eq!(field.data_type, KalamDataType::Text);
136        assert_eq!(field.index, 1);
137        assert_eq!(field.flags, None);
138    }
139
140    #[test]
141    fn test_schema_field_with_flags() {
142        let mut flags = FieldFlags::new();
143        flags.insert(FieldFlag::PrimaryKey);
144        flags.insert(FieldFlag::NonNull);
145        flags.insert(FieldFlag::Unique);
146        let field = SchemaField::new("id", KalamDataType::Uuid, 0).with_flags(flags);
147        let json = serde_json::to_string(&field).unwrap();
148        assert!(json.contains("\"flags\":["));
149        assert!(json.contains("\"pk\""));
150        assert!(json.contains("\"nn\""));
151        assert!(json.contains("\"uq\""));
152    }
153
154    #[test]
155    fn test_schema_field_deserializes_flags_array() {
156        let json = r#"{"name":"id","data_type":"Uuid","index":0,"flags":["pk","nn","uq"]}"#;
157        let field: SchemaField = serde_json::from_str(json).unwrap();
158        assert!(matches!(
159            field.flags,
160            Some(flags)
161                if flags.contains(&FieldFlag::PrimaryKey)
162                    && flags.contains(&FieldFlag::NonNull)
163                    && flags.contains(&FieldFlag::Unique)
164        ));
165    }
166
167    #[test]
168    fn test_schema_field_with_embedding() {
169        let field = SchemaField::new("vector", KalamDataType::Embedding(384), 5);
170        let json = serde_json::to_string(&field).unwrap();
171        assert!(json.contains("\"Embedding\":384"));
172    }
173
174    #[test]
175    fn test_schema_field_with_decimal() {
176        let field = SchemaField::new(
177            "price",
178            KalamDataType::Decimal {
179                precision: 10,
180                scale: 2,
181            },
182            2,
183        );
184        let json = serde_json::to_string(&field).unwrap();
185        assert!(json.contains("\"Decimal\""));
186    }
187}